排序演算法下——桶排序、計數排序和基數排序
桶排序、計數排序和基數排序這三種演算法的時間複雜度都為 ,因此,它們也被叫作線性排序(Linear Sort)。之所以能做到線性,是因為這三個演算法是 非基於比較 的排序演算法,都不涉及元素之間的比較操作。
1. 桶排序(Bucket Sort)?
1.1. 桶排序原理
- 桶排序 ,顧名思義,要用到“桶”。核心思想是將要排序的資料分到幾個有序的桶裡,每個桶的資料再單獨進行排序。桶內排完序後,再把每個桶裡的資料按照順序依次取出,組成的序列就是有序的了。

1.2. 桶排序的時間複雜度分析
- 如果要排序的資料有 個,我們把它們均勻地劃分到 個桶內,每個桶內就有 個元素。對每個桶內的資料進行快速排序,時間複雜度為 。 個桶排序時間複雜度就為 。當桶的個數接近資料個數時, 就是一個非常小的數,這個時候通排序的時間複雜度接近於 。
1.3. 桶排序的適用條件
桶排序看起來很優秀,但事實上,桶排序對排序資料的要求是非常苛刻的。
- 首先, 要排序的資料需要很容易就能劃分為 個桶,並且桶與桶之間有著天然的大小順序 。
- 其次, 資料在各個桶之間的分佈是比較均勻的 。
- 桶排序比較適合用在 外部排序 中。所謂的外部排序就是資料儲存在外部磁碟中,資料比較大而記憶體有限,無法將資料全部載入到記憶體中去。
1.4. 一個桶排序的例項
假如我們有 10 GB 的訂單資料需要按照金額進行排序,但記憶體只有幾百 MB ,這時候該怎麼辦呢?
-
我們先掃描一遍檔案,確定訂單金額的資料範圍。
-
如果掃描後發現訂單金額處於 1 萬元到 10 萬元之間,我們將所有訂單按照金額劃分到 100 個桶內,第一個桶資料範圍為[1, 1000],第二個桶資料範圍為[1001, 2000]......,每個桶對應一個檔案,同時將檔案按照金額範圍的大小順序編號命名(如00、01、02...99)。
-
如果訂單金額分佈均勻,則每個檔案包含大約 100 MB 的資料,我們可以將每個小檔案讀入到記憶體中,進行快速排序。然後,再按順序從各個小檔案讀取資料,寫入到另外一個檔案,即是排序好的資料了。
-
如果訂單分佈不均,某一範圍內資料特別多無法一次讀入記憶體,則可以繼續對此區間再進行劃分,直到所有的檔案都可以讀入記憶體為止。
2. 計數排序(Counting Sort)?
2.1. 計數排序演算法實現
-
計數排序可以看作是桶排序的一種特殊情況。當要排序的資料所處的範圍並不大時,比如最大值為 ,這時候,我們可以把資料分為 個桶,每個桶內的資料都是相同的,省掉了桶內排序的時間。
-
假設高考分數的範圍為 [0, 750],我們可以將考生的分數劃分到 751 個桶內,然後再依次從桶內取出資料,就可以實現對考生成績的排序了。因為只涉及到掃描遍歷操作,因此時間複雜度為 。
但這個排序演算法為什麼叫計數排序呢,這是由計數排序演算法的實現方法來決定的,我們來看一個簡單的例子。
-
假設有 8 個考生,他們的分數範圍為 [0, 5]。這 8 個考生的成績我們放在一個數組中,A[8] = {2, 5, 3, 0, 2, 3, 0, 3}。
-
我們用大小為 6 的陣列代表 6 個桶來統計考生的成績分佈情況,其中 下標表示考生的分數,陣列內的值表示這個分數的考生個數 。我們遍歷一遍陣列後,就可以得到 C[6] = {2, 0, 2, 3, 0, 1,},得 0 分的共有 2 人,得 3 分的共有 3 人。
-
如下所示,成績為 3 的考生共有 3 個,小於 3 分的考生共有 4 個,所以在排好序的資料 R[8] 中,3 的位置應該為 4,5 和 6 。

- 而我們怎麼得到這個位置呢?只需要對 C[6] 陣列按順序累計求和即可,這時候,C[6] 陣列中的每個值就都表示 小於等於它的值的個數 了。

-
接下來,我們從後到前依次掃描陣列 A[8]。當掃描到 3 時,我們取出 C[3] 的值 7,說明小於等於 3 的個數為 7 個,那麼 3 就應該放在陣列 R[8] 的第 7 個位置,也就是下標為 6 的地方。當我們再次遇到 3 的時候,這時候小於等於 3 的元素個數就少了一個,也就是我們要把 C[3] 相應地減去 1 。
-
之所以要從後到前依次掃描陣列,是因為這樣 之前相同的元素就仍然會保持相同的順序,可以保證排序演算法的穩定性 。
-
當我們掃描完整個陣列 A[8] 時,陣列 R[8] 中的資料也就從小到大排好序了,其詳細過程可參考下圖。

- 程式碼實現
// 假設陣列中儲存的都是非負整數 void Counting_Sort(int data[], int n) { if (n <= 1) { return; } // 尋找陣列的最大值 int max = data[0]; for (int i = 1; i < n; i++) { if (data[i] > max) { max = data[i]; } } // 定義一個計數陣列, 統計每個元素的個數 int c[max+1] = {0}; for (int i = 0; i < n; i++) { c[data[i]]++; } // 對計數陣列累計求和 for (int i = 1; i <= max; i++) { c[i] = c[i] + c[i-1]; } // 臨時存放排好序的資料 int r[n] = {0}; // 倒序遍歷陣列,將元素放入正確的位置 for (int i = n-1; i >= 0; i--) { r[c[data[i]] - 1] = data[i]; c[data[i]]--; } for (int i = 0; i < n; i++) { data[i] = r[i]; } } 複製程式碼
2.2. 計數排序的適用範圍
-
計數排序只適用於 資料範圍不大的場景中 ,如果資料範圍 比排序的資料 大很多,就不適合用計數排序了。
-
計數排序能給 非負整數排序 ,如果資料是其他型別的,需要將其在不改變相對大小的情況下,轉化為非負整數。比如資料有一位小數,我們需要將資料都乘以 10;資料範圍為 [-1000, 1000],我們需要對每個資料加 1000。
3. 基數排序(Radix Sort)?
假設要對 10 萬個手機號碼進行排序,顯然桶排序和計數排序都不太適合,那怎樣才能做到時間複雜度為 呢?
1.1. 基數排序原理
-
手機號碼有這樣的規律,假設要比較兩個手機號碼 的大小,如果在前面幾位中, 手機號碼已經比 大了,那後面幾位就不用看了。
-
藉助 穩定排序演算法 ,我們可以這麼實現。從手機號碼的最後一位開始,分別按照每一位的數字對手機號碼進行排序,依次往前進行,經過 11 次排序之後,手機號碼就都有序了。
-
下面是一個字串的排序例項,和手機號碼類似。

-
根據每一位的排序,我們可以用剛才的桶排序或者計數排序來實現,它們的時間複雜度可以做到 。如果排序的資料有 位,則總的時間複雜度為 ,當 不大時,基數排序的時間複雜度就近似為 。
-
有時候,要排序的資料並不都是等長的,比如我們要對英文單詞進行排序。這時候,我們可以 把所有單詞都補足到相同長度,位數不夠的在後面補 ’0‘ ,所有字母的 ASCII 碼都大於 ‘0’,因此不會影響原有的大小順序。
-
基數排序需要資料可以分割出獨立的位出來,而且位之間有遞進的關係。除此之外,每一位的資料範圍都不能太大,要可以用線性排序演算法來進行排序。
ofollow,noindex">參考資料-極客時間專欄《資料結構與演算法之美》
獲取更多精彩,請關注「seniusen」!
