【八大排序算法】16張圖帶你完全搞懂基數排序

原創公衆號:bigsai 轉載需聯繫做者

前言

在排序算法中,你們可能對桶排序、計數排序、基數排序不太瞭解,不太清楚其算法的思想和流程,也可能看過會過可是很快就忘記了,可是沒關係,幸運的是你看到了本篇文章。本文將通俗易懂的給你講解基數排序。java

基數排序,是一種原理簡單,但實現複雜的排序。不少人在學習基數排序的時候可能會遇到如下兩種狀況而淺嘗輒止:git

  • 一看原理,這麼簡單,懂了懂了(順便溜了)
  • 再一看代碼,這啥啥啥啊?這些的確定有問題(不看溜了)

    image-20201113205712629

要想深刻理解基數排序,必須搞懂基數排序各類形式(數字類型、等長字符類型、不等長字符)各自實現方法,瞭解其中的聯繫和區別,而且也要掌握空間優化的方法(非二維數組而僅用一維數組)。下面跟着我詳細學習基數排序吧!算法

基數排序原理

首先百度百科看看基數排序的定義:數組

基數排序(radix sort)屬於「分配式排序」(distribution sort),又稱「桶子法」(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些「桶」中,藉以達到排序的做用,基數排序法是屬於穩定性的排序,基數排序法的效率高於其它的穩定性排序法。

基數排序也稱爲卡片排序,簡而言之,基數排序的原理就是屢次利用計數排序(計數排序是一種特殊的桶排序),可是和前面的普通桶排序和計數排序有所區別的是,基數排序並非將一個總體分配到一個桶中,而是將自身拆分紅一個個組成的元素,每一個元素分別順序分配放入桶中、順序收集,當從前日後或者從後往前每一個位置都進行過這樣順序的分配、收集後,就得到了一個有序的數列。微信

在具體實現上若是從左往右那就是最高位優先(Most Significant Digit first)法,簡稱MSD法;若是從右往左那就是最低位優先(Least Significant Digit first)法,簡稱LSD法。可是無論從最高位開始仍是從最低位開始要保證和相同位進行比較,你須要注意的是若是是int等數字類型須要保證從右往左(從低位到高位)保證對齊,若是是字符類型的話須要從左往右(從高位到低位)保證對齊。ide

image-20201113154119682

你可能會問爲啥不直接將這個數或者這個數按照區間範圍放到對應的桶中,一方面基數排序可能不少時候處理的是字符型的數據,不方便放入某個桶中,另外一方面若是數字很大,不方便直接放入桶中。而且基數排序並不須要交換,也不須要比較,就是屢次分配、收集獲得結果。學習

image-20201113150949762

因此遇到這種狀況咱們基數排序思想很簡單,就拿 934,241,3366,4399這幾個數字進行基數排序的一趟過程來看,第一次會根據各位進行分配、收集:優化

image-20201113161050871

分配和收集都是有序的,第二次會根據十位進行分配、收集,這次是在第一次個位分配、收集基礎上進行的,因此全部數字單看個位十位是有序的。spa

image-20201113161752292

而第三次就是對百位進行分配收集,這次完成以後百位及其如下是有序的。3d

image-20201113162803486

而最後一次的時候進行處理的時候,千位有的數字須要補零,此次完畢後後千位及之後都有序,即整個序列排序完成。

image-20201113170715860

想必看到這裏基數排序的思想你也已經懂了吧,可是雖然懂你不必定可以寫出代碼來,繼續看看下面的分析和實現。

數字類型基數排序

有不少時候也有不少時候對基數排序的講解也是基於數字類型的,而數字類型這裏就用int來實現,對於數字類型的基數排序你須要注意的有如下幾點:

  • 不管是最高位優先法仍是最低位優先法進行遍歷須要保證數字各位、十位、百位等對齊,這裏我使用最低位優先法從個位開始向上。
  • 數字類型的基數排序須要十個桶(0-9),你可使用二維數組,第一維度長度爲10表示十個數字,第二個維度爲數組長度,用來存儲數字(由於最壞狀況可能當前位數字同樣)。但這樣無疑太浪費內存空間了,你可使用List或者Queue替代,這裏就用List了。
  • 具體實現要先找到最大值肯定最高多少位,用來進行遍歷時候確認。
  • 收集的時候藉助一個自增參數遍歷收集。
  • 每次收集完畢十個桶(bucket)須要清空待下次收集。

實現的代碼爲:

static void radixSort(int[] arr)//int 類型 從右往左
{
  List<Integer>bucket[]=new ArrayList[10];
  for(int i=0;i<10;i++)
  {
    bucket[i]=new ArrayList<Integer>();
  }
  //找到最大值
  int max=0;//假設都是正數
  for(int i=0;i<arr.length;i++)
  {
    if(arr[i]>max)
      max=arr[i];
  }
  int divideNum=1;//1 10 100 100……用來求對應位的數字
  while (max>0) {//max 和num 控制
    for(int num:arr)
    {
      bucket[(num/divideNum)%10].add(num);//分配 將對應位置的數字放到對應bucket中
    }
    divideNum*=10;
    max/=10;
    int idx=0;
    //收集 從新撿起數據
    for(List<Integer>list:bucket)
    {
      for(int num:list)
      {
        arr[idx++]=num;
      }
      list.clear();//收集完須要清空留下次繼續使用
    }
  }
}

等長字符串基數排序

除了數字以外,等長字符串也是經常遇到的方式,其主要方法和數字類型差很少,這裏也看不出策略上的不一樣。低位優先法或者高位優先法均可使用(這裏依舊低位從右向左)。

image-20201113182852797

在實現細節方面,和前面的數字類型區別不是很大,可是由於字符串是等長的遍歷更加方便容易。但須要額外注意的是:

  • 字符類型的桶bucket不是10個而是ASCII字符的個數(根據實際須要查看ASCII表)。其實就是利用char和int之間關係能夠直接按照每一個字符進行順序存儲。

具體實現代碼爲:

static void radixSort(String arr[],int len)//等長字符排序狀況 長度爲len
{
  List<String>buckets[]=new ArrayList[128];
  for(int i=0;i<128;i++)
  {
    buckets[i]=new ArrayList<String>();
  }
  for(int i=len-1;i>=0;i--)//每一位上進行操做
  {
    for(String str:arr)
    {
      buckets[str.charAt(i)].add(str);//分配
    }
    int idx=0;
    for(List<String>list:buckets)
    {
      for(String str:list)
      {
        arr[idx++]=str;//收集
      }
      list.clear();//收集完該bucket清空
    }
  }
}

非等長字符串基數排序

等長的字符串進行基數排序時候很好遍歷,那麼非等長的時候該如何考慮呢?這種非等長不能像處理數字那樣粗暴的計算當成0便可。字符串的大小是從前日後進行排列的(和長度不要緊)。例如看下面字符串,「d」這個字符串即便很短可是在排序依然放在最後面。你知道該怎麼處理嗎?

"abigsai"
"bigsai"
"bigsaisix"
"d"

若是高位優先,前面一旦比較過各個字符的桶(bucket)就要固定下來,也就是在進行右面下一個字符分配、收集的時候要標記空間,即下次進行分配收集的前面是‘a’字符的一組,‘b’字符一組,而且不能越界,實現起來很麻煩這裏就不詳細講解了有興趣的能夠自行研究一下。

而本篇實現的是低位優先。低位優先採用什麼思路呢?很簡單,跟我看圖解。

第一步,先將字符按照長度進行分配到一個桶(bucket)中,聲明一個List<String>wordLen[maxlen+1];在遍歷字符時候,以字符長度爲下表index,將字符串順序加入進去。其中maxlen爲最長字符串長度,之因此要maxlen+1是由於須要使用maxlen下標(0-maxlen)。

image-20201113190245500

第二步,分配完成遍歷收集到原數組中,這樣原數組在長度上相對有序

image-20201113190606255

這樣就能夠進行基數排序啦,固然,在開始的時候並非所有都進行分配收集,而是根據長度慢慢遞減,長度能夠到達6位分配、收集,長度到達5的分配、收集……長度爲1的進行分配、收集。這樣進行一遭就很完美的進行完基數排序,由於咱們藉助根據長度收集的桶能夠很容易知道當前長度開始的index在哪裏。

image-20201113192740924

具體實現的代碼爲:

static void radixSort(String arr[])//字符不等長的狀況進行排序
{
    //找到最長的那個
    int maxlen=0;
    for(String team:arr)
    {
        if(team.length()>maxlen)
            maxlen=team.length();
    }
    //一個對長度分  一個對具體字符分,先用長度來找到
    List<String>wordLen[]=new ArrayList[maxlen+1];//用長度先統計各個長度的單詞
    List<String>bucket[]=new ArrayList[128];//根據字符來劃分
    for(int i=0;i<wordLen.length;i++)
        wordLen[i]=new ArrayList<String>();
    for(int i=0;i<bucket.length;i++)
        bucket[i]=new ArrayList<String>();
    //先根據長度來一下
    for(String team:arr)
    {
        wordLen[team.length()].add(team);
    }
    int index=0;//先進行一次(按照長度分)的桶排序使得數組長度初步有序
    for(List<String>list:wordLen)
    {
        for(String team:list)
        {
            arr[index++]=team;
        }
    }
    //而後 先進行長的 從後往前進行
    int startIndex=arr.length;
    for(int len=maxlen;len>0;len--)//每次長度相同的要進行基數一次
    {
        int preIndex=startIndex;
        startIndex-=wordLen[len].size();
        for(int i=startIndex;i<arr.length;i++)
        {
            bucket[arr[i].charAt(len-1)].add(arr[i]);//利用字符桶從新裝
        }
        //從新收集
        index=startIndex;
        for(List<String>list:bucket)
        {
            for(String str:list)
            {
                arr[index++]=str;
            }
            list.clear();
        }
    }
}

空間優化(等長字符)基數排序

上面不管是等長仍是不等長,使用的空間其實都是跟二維相關的,咱們能不能使用一維的空間去解決這個問題呢?固然能啊。

在使用空間的整個思路是差很少的,可是這裏爲了讓你可以理解咱們在講解的時候講解等長字符串的狀況

先回憶剛剛講的等長字符串,就是從個位進行遍歷,在遍歷的時候將數據放到對應的桶裏面,而後在進行收集的時候放回原數組。

image-20201113195501579

你可否發現什麼規律

  • 一個字符串收集的時候放的位置其實它只須要知道它前面有多少個就能夠肯定
  • 而且當前位置字符若是相同那麼就是根據arr中相對順序來進行當前輪。

因此咱們能夠嘗試來動態維護這個int bucket[]。第一次進行只記錄次數,第二次進行疊加表示比當前位置+1編號小的元素的個數。

image-20201113200950104

可是這樣處理不太好知道比當前位置小的有多少,因此咱們在分配的時候向下挪一位,這樣bucket[i]就能夠表示比當前位置小的元素的個數。

image-20201113201809349

咱們在進行收集的時候須要再次遍歷arr,但咱們須要一個臨時數組String value[]儲存結果(由於arr沒遍歷完後面不能使用),而進行遍歷的規則就是:遍歷arr時候對應字符串str,該位字符對應bucket[str.charAt(i)]桶中數字就是要放到arr中的編號(多少個比它小的就放到第多少位),放置以後要對bucket當前位自增(由於下一個這個位置字符串要把這個str考慮進去)。這樣到最後便可完成排序。

第一趟遍歷arr前兩個字符串部分過程以下:

image-20201113203419472

第一趟中間兩個字符串處理狀況:

image-20201113203931193

第一趟最後兩個字符串處理狀況:

image-20201113204444889

就這樣便可完成一趟操做,一趟完成記得將value的值賦值到arr中,固然有方法使用指針引用能夠避免交換數據帶來的時間影響,但這裏爲了使你們更加簡單理解就直接複製過去。這樣完成若干次,整個基數排序便可完成。

具體實現的代碼爲:

static void radixSortByArr(String arr[],int len)//固定長度的使用數組進行優化
{
    int charLen=129;//多用一個

    String value[]=new String[arr.length];
    for(int index=len-1;index>=0;index--)//不一樣的位置
    {
        int bucket[]=new int[charLen];//儲存character的桶
        for(int i=0;i<arr.length;i++)//分配
        {
            bucket[(int)(arr[i].charAt(index)+1)]++;
        }
        for(int i=1;i<bucket.length;i++)//疊加 當前i位置表示比本身小的個數
        {
            bucket[i]+=bucket[i-1];
        }

        for(int i=0;i<arr.length;i++)
        {
            value[bucket[arr[i].charAt(index)]++]=arr[i];//中間的++由於當前位置填充了一個,下次再來同元素就要後移
        }
        System.arraycopy(value,0,arr,0,arr.length);//copy數組
    }
}

至於不定長的,思路也差很少,這裏就留給你優秀的你本身去思考啦。

結語

至於基數排序的算法分析,以定長的狀況分析,假設有n數字(字符串),每一個有k位,那麼根據基數就要每一位都遍歷就是K次,每次都是O(n)級別。因此差很少是O(n*k)級別,固然k遠遠小於n,可能有成千上萬個數,可是每一個數或者字符正常可沒成千上萬那麼長。

本次基數排序就全講完啦,那麼多張圖我想你也應該懂了。

最後我請大家兩連事幫忙一下:

  1. 點贊、關注一下支持, 您的確定是我在平臺創做的源源動力。
  2. 微信搜索「bigsai」,關注個人公衆號,不只免費送你電子書,我還會第一時間在公衆號分享知識技術。加我還可拉你進力扣打卡羣一塊兒打卡LeetCode。

記得關注、我們下次再見!

image-20201114211553660

相關文章
相關標籤/搜索