前言
基數排序(radix sort)又稱桶排序(bucket sort),相對於常見的比較排序,基數排序是一種分配式排序,即通過將所有數字分配到應在的位置最后再覆蓋到原數組完成排序的過程。我在上一篇講到的計數排序也屬於這種排序模式,上一篇結尾處提到了計數排序的穩定性,即排序前和排序后相同的數字相對位置保持不變。今天我們要說的基數排序就要利用到排序穩定性這一點。
思考過程
我們回想一下我們小時候是怎么學習比較數字大小的?我們是先比位數,如果一個位數比另一個位數多,那這個數肯定更大。如果位數同樣多,就按位數遞減依次往下進行比較,哪個數在這一位上更大那就停止比較,得出這個在這個位上數更大的數字整體更大的結論。當然我們也可以從最小的位開始比較,這其實就對應了基數排序里的MSD(most significant digital)和LSD(least significant digital)兩種排序方式。
想清楚了這一點之后,我們就要考慮如何存儲每一位排序結果的問題了,首先既然作為分配式排序,聯想計數排序,每一位排序時存儲該次排序結果的數據結構應該至少是一個長度為10的數組(對應十進制該位0-9的數字)。同時可能存在以下情況:原數組中所有元素在該位上的數字都相同,那一維數組就沒法滿足我們的需要了,我們需要一個10*n(n為數組長度)的二維數組來存儲每次位排序結果。熟悉計數排序結果的讀者可能會好奇:為什么不能像計數排序一樣,在每個位置只存儲出現該數字的次數,而不存儲具體的值,這樣不就可以用一維數組了?這個我們不妨先思考一下,在對基數排序分析完之后再來看這個問題。
現在我們可以存儲每次位排序的結果了,為了在下一位排序前用到這一位排序的結果,我們要將桶里排序的結果還原到原數組中去,然后繼續對更改后的原數組執行前一步的位排序操作,如此循環,最后的結果就是數組內元素先按最高位排序,最高位相同的依次按下一位排序,依次遞推。得到排序的結果數組。
算法過程
- 初始化:構造一個10*n的二維數組,一個長度為n的數組用於存儲每次位排序時每個桶子里有多少個元素。
- 循環操作:從低位開始(我們采用LSD的方式),將所有元素對應該位的數字存到相應的桶子里去(對應二維數組的那一列)。然后將所有桶子里的元素按照桶子標號從小到大取出,對於同一個桶子里的元素,先放進去的先取出,后放進去的后取出(保證排序穩定性)。這樣原數組就按該位排序完畢了,繼續下一位操作,直到最高位排序完成。
下面給出一個實例幫助理解:
我們現有一個數組:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
下面是排序過程(二維數組里每一列對應一個桶,因為桶空間沒用完,因此沒有將二維數組畫全):
1.按個位排序
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
|
81 |
22 |
73 |
14 |
55 |
|
|
28 |
39 |
|
|
|
|
93 |
|
65 |
|
|
|
|
|
|
|
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
按第一位排序后數組結果:
81,22,73,93,43,14,55,65,28,39
可以看到數組已經按個位排序了。
2根據個位排序結果按百位排序
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
|
14 |
22 |
39 |
43 |
55 |
65 |
73 |
81 |
93 |
|
|
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
取出排序結果:
14,22,28,39,43,55,65,73,81,93
可以看到在個位排序的基礎上,百位也排序完成(對於百位相同的數子,如22,28,因為個位已經排序,而取出時也保持了排序的穩定性,所以這兩個數的位置前后是根據他們個位排序結果決定的)。因為原數組元素最高只有百位,原數組也完成了排序過程。
總結
我們現在來看看之前遺留的兩個問題:為什么不能用一維數組,一定要用二維數組這樣的類似桶的結構來存儲中間位排序結果?其實之所以要寫這個問題,是因為我覺得這個問題是理解基數排序的關鍵。基數排序本身原理很簡單,但是實現中有兩個問題需要考慮:1.怎么保留前一位的排序結果,這個問題用之前提到的排序穩定性可以解決。2.怎么關聯該位排序結果和原數組元素,二維數組正是為了解決這個問題使用的辦法。在計數排序里,雖然保留了所有相等的元素的相對位置,但是這些相等的元素在計數排序里實際是沒有差別的,因此我們可以只保存數組里有多少個這樣的元素即可。而基數排序里不同,有些元素雖然在某一位上相同,但是他們其他位上很可能不同,如果只保存該位上有多少個5或者多少個6,那關於元素其他位的信息就都丟棄了,這樣也就沒法對這些元素更高位進行排序了。
弄清基數排序的過程后,我們來看看這個算法的時間復雜度是多少?每次循環遍歷數組將元素放在指定位置Θ(n),在從桶中取出數據Θ(n),循環d次(d是位數),時間復雜度就是Θ(r*n)
最后附上基數排序的java實現:
package sort; public class RadixSort { private static void radixSort(int[] array,int d) { int n=1;//代表位數對應的數:1,10,100... int k=0;//保存每一位排序后的結果用於下一位的排序輸入 int length=array.length; int[][] bucket=new int[10][length];//排序桶用於保存每次排序后的結果,這一位上排序結果相同的數字放在同一個桶里 int[] order=new int[length];//用於保存每個桶里有多少個數字 while(n<d) { for(int num:array) //將數組array里的每個數字放在相應的桶里 { int digit=(num/n)%10; bucket[digit][order[digit]]=num; order[digit]++; } for(int i=0;i<length;i++)//將前一個循環生成的桶里的數據覆蓋到原數組中用於保存這一位的排序結果 { if(order[i]!=0)//這個桶里有數據,從上到下遍歷這個桶並將數據保存到原數組中 { for(int j=0;j<order[i];j++) { array[k]=bucket[i][j]; k++; } } order[i]=0;//將桶里計數器置0,用於下一次位排序 } n*=10; k=0;//將k置0,用於下一輪保存位排序結果 } } public static void main(String[] args) { int[] A=new int[]{73,22, 93, 43, 55, 14, 28, 65, 39, 81}; radixSort(A, 100); for(int num:A) { System.out.println(num); } } }
下面是程序運行結果:

