簡單介紹
基數排序(radix sort)屬於 分配式排序(distribution sort),又稱 桶子法(bucket sort 或 bin sort),顧名思義,它是通過鍵值的各個位的值,將要排序的 元素分配 至某些「桶」中,達到排序的作用。基數排序是對 傳統桶排序 的擴充套件。
基數排序屬於 穩定性 的排序,基數排序法是效率高的穩定性排序法。
其時間複雜度為O (nlog(r)m),其中r為所採取的基數,而m為堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。
穩定性簡介
2,1,43,1 陣列進行排序後變成:1,1,2,43
穩定性指的是:兩個 1 的先後順序不改變。
基數排序(Radix Sort)是 桶排序 的擴充套件。
基數排序是 1887 年赫爾曼·何樂禮發明的。實現方式:將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
基本思想
- 將所有待比較數值 統一為同樣的數位長度,數位較短的數 前面補零
- 然後從最低位開始(個位),依次進行一次排序
- 這樣從最低位排序一直到最高位排序完成以後,序列就變成了一個有序序列
基本思想是抽象的,下面看看思路分析,你就明白是咋回事了。
思路分析
解析上面的圖:
第一輪:判斷 個位數
-
將每個元素的 個位數 取出,找到其 個位數 所對應的下標的桶,然後把這個數放到桶中(桶為一個一維陣列)
-
按照這個桶的順序,依次取出資料,放回原來的陣列
注意:取出桶裡的資料時,不僅要按桶的順序依次來取,而且單個桶裡的資料取出順序也要按先放進去的先取出來的規則。
以上步驟中,每一輪除了用什麼位數的值來判斷放在哪個桶裡不同外,其他的都相同。
第二輪:判斷 十位數
。
需要注意的是:
- 第一輪使用後的桶並未清理,上圖為了講解方便,並未展示桶中已有的資料,不過會進行覆蓋。
- 長度不足的數,用零表示。如
3
,沒有十位數,則歸類到第一個桶中(0),即該數看成03
。
第三輪:判斷 百位數
一個流程下來,你會發現,在第三輪排序後,陣列已經是有序的了
動圖:
程式碼實現
推導實現
為了更容易理解,下面進行分步推導來找出裡面的規律,從而實現演算法。
/**
* 推導:推導每一步的狀態,然後找規律
*/
@Test
public void processDemo() {
//這裡我就隨便一個陣列了
int arr[] = {53, 3, 542, 748, 14, 214};
System.out.println("原始陣列:" + Arrays.toString(arr));
processRadixSort(arr);
}
public void processRadixSort(int[] arr) {
// 第一輪
// 1. 將每個元素的 **個位數** 取出,找到其 個位數 所對應的下標的桶,然後把這個數放到桶中(**桶為一個一維陣列**)
// 2. 按照這個桶的順序,**依次取出資料**,放回原來的陣列
// 定義 10 個桶,每個桶是一個一維陣列
// 由於無法知道每個桶需要多少個元素,所以宣告為陣列長度
// 例如:加入10 個數字都是 1,那麼只會分配到同一個通中
int[][] buckets = new int[10][arr.length];
// 定義每個桶中有效的資料個數
// 桶長度為陣列大小,那麼每一個桶中存放了幾個有效的元素呢?就需要有這個變數來指示
int[] bucketCounts = new int[buckets.length];
// 開始第一輪的程式碼實現
// 1. 將每個元素的 **個位數** 取出,找到其 個位數 所對應的下標的桶,然後把這個數放到桶中(**桶為一個一維陣列**)
for (int i = 0; i < arr.length; i++) {
// 獲取到個位數
int temp = arr[i] % 10;
// 根據規則,將該數放到對應的桶中
buckets[temp][bucketCounts[temp]] = arr[i];
// 並將該桶的有效個數+1
bucketCounts[temp]++;
}
// 2. 按照這個桶的順序,**依次取出資料**,放回原來的陣列
int index = 0; // 原始陣列下標索引
for (int i = 0; i < buckets.length; i++) {
if (bucketCounts[i] == 0) {
// 標識該桶無資料
continue;
}
for (int j = 0; j < bucketCounts[i]; j++) {
arr[index++] = buckets[i][j];
}
// 取完資料後,要重置每個桶的有效資料個數,注意,這裡一步一定要做!!!
bucketCounts[i] = 0;
}
System.out.println("第一輪排序後:" + Arrays.toString(arr));
// 第 2 輪:判斷 十位數
for (int i = 0; i < arr.length; i++) {
// 獲取到十位數
int temp = arr[i] / 10 % 10;//就這一步不同
buckets[temp][bucketCounts[temp]] = arr[i];
bucketCounts[temp]++;
}
index = 0; // 原始陣列下標索引
for (int i = 0; i < buckets.length; i++) {
if (bucketCounts[i] == 0) {
continue;
}
for (int j = 0; j < bucketCounts[i]; j++) {
arr[index++] = buckets[i][j];
}
bucketCounts[i] = 0;
}
System.out.println("第二輪排序後:" + Arrays.toString(arr));
// 第 3 輪:判斷 百位數
for (int i = 0; i < arr.length; i++) {
// 獲取到十位數
int temp = arr[i] / 100 % 10;//就這一步不同
buckets[temp][bucketCounts[temp]] = arr[i];
bucketCounts[temp]++;
}
index = 0; // 標識當前放回原陣列的哪一個了
for (int i = 0; i < buckets.length; i++) {
if (bucketCounts[i] == 0) {
continue;
}
for (int j = 0; j < bucketCounts[i]; j++) {
arr[index++] = buckets[i][j];
}
bucketCounts[i] = 0;
}
System.out.println("第三輪排序後:" + Arrays.toString(arr));
}
測試輸出
原始陣列:[53, 3, 542, 748, 14, 214]
第一輪排序後:[542, 53, 3, 14, 214, 748]
第二輪排序後:[3, 14, 214, 542, 748, 53]
第三輪排序後:[3, 14, 53, 214, 542, 748]
根據前面的推導,整理發現有如下的規律:整體程式碼比較固定,少數變數在變化,即判斷的位數
-
要迴圈幾輪?這個與待排序陣列中的最大值有幾位數有關係
需要找到陣列中的最大值,並且得到該值的位數
-
獲取陣列中每個數的個、十、百 位數的公式可以如下整理:
// 獲取個位數 arr[i] % 10 -> arr[i] / 1 % 10 // 獲取十位數 arr[i] / 10 % 10 // 獲取百位數 arr[i] / 100 % 10
可以發現規律,每一次變化的都是 10 的倍數
因此通過推到以及上面的分析整理,下面完成了該演算法的具體程式碼
完整實現
@Test
public void radixSortTest() {
int arr[] = {53, 3, 542, 748, 14, 214};
System.out.println("原始陣列:" + Arrays.toString(arr));
radixSort(arr);
System.out.println("排序後:" + Arrays.toString(arr));
}
/**
* 根據推導規律,整理出完整演算法
*
* @param arr
*/
public void radixSort(int[] arr) {
// 1. 得到陣列中的最大值,並獲取到該值的位數。用於知道要迴圈幾輪
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 得到位數
int maxLength = (max + "").length();//這裡很巧妙!
// 定義桶 和 標識桶中元素個數
int[][] bucket = new int[10][arr.length];
int[] bucketCounts = new int[bucket.length];
// 總共需要進行 maxLength 輪
for (int k = 1, n = 1; k <= maxLength; k++, n *= 10) {//注意看這裡,有兩個變數,k控制迴圈,n用於後面獲取到對應的位數
// 進行桶排序
for (int i = 0; i < arr.length; i++) {
// 獲取該輪的桶索引:n 每一輪按 10 的倍數遞增,獲取到對應數位數
// 這裡額外使用一個步長為 10 的變數 n 來得到每一次遞增後的值
int bucketIndex = arr[i] / n % 10;//這裡很巧妙!
// 放入該桶中
bucket[bucketIndex][bucketCounts[bucketIndex]] = arr[i];
// 標識該桶元素多了一個
bucketCounts[bucketIndex]++;
}
// 將桶中元素獲取出來,放到原陣列中,注意,要按單個桶中的資料取出是先進先出的順序
int index = 0;
for (int i = 0; i < bucket.length; i++) {
if (bucketCounts[i] == 0) {
// 該桶無有效元素,跳過不獲取
continue;
}
// 獲取桶中有效的個數
for (int j = 0; j < bucketCounts[i]; j++) {
arr[index++] = bucket[i][j];
}
// 取完後,重置該桶的元素個數為 0 ,下一次才不會錯亂資料,這一步很重要!!!
bucketCounts[i] = 0;
}
System.out.println("第" + k + "輪排序後:" + Arrays.toString(arr));
}
}
測試輸出
原始陣列:[53, 3, 542, 748, 14, 214]
第1輪排序後:[542, 53, 3, 14, 214, 748]
第2輪排序後:[3, 14, 214, 542, 748, 53]
第3輪排序後:[3, 14, 53, 214, 542, 748]
排序後:[3, 14, 53, 214, 542, 748]
動圖:
大資料量耗時測試
/**
* 大量資料排序時間測試
*/
@Test
public void bulkDataSort() {
int max = 80000;
// max = 8;
int[] arr = new int[max];
for (int i = 0; i < max; i++) {
arr[i] = (int) (Math.random() * 80000);
}
if (arr.length < 10) {
System.out.println("原始陣列:" + Arrays.toString(arr));
}
Instant startTime = Instant.now();
radixSort(arr);
if (arr.length < 10) {
System.out.println("排序後:" + Arrays.toString(arr));
}
Instant endTime = Instant.now();
System.out.println("共耗時:" + Duration.between(startTime, endTime).toMillis() + " 毫秒");
}
多次測試輸出資訊
共耗時:31 毫秒
共耗時:29 毫秒
共耗時:22 毫秒
共耗時:39 毫秒
如果增加資料量到 800 萬,也發現只會用時 400 毫秒左右,速度非常快。
但是,如果資料量增加到 8000 萬,則會報錯(這個就得看你的電腦記憶體大小了)
java.lang.OutOfMemoryError: Java heap space
at cn.mrcode.study.dsalgtutorialdemo.datastructure.sort.radix.RadixSortTest.radixSort(RadixSortTest.java:125)
這是為什麼呢?原因就在於開啟了 10 個桶,每個桶都是 8000 萬個資料。那麼換算下單位:
80000000 * 11 * 4 / 1024 /1024 / 1024 = 3.2 G 左右的堆空間
# 11 = 10 個桶 + 原始陣列
# 4 :一個 int 佔用 4 位元組
也就是說,犧牲記憶體來提升速度。
注意事項
-
基數排序是對 傳統桶排序 的擴充套件,速度很快
-
是經典的空間換時間的方式,佔用記憶體空間很大
當資料量太大的時候,所耗費的額外空間較大。是原始資料的 10 倍空間
-
基數排序是穩定的
相同的數,排序之後,他們的先後順序沒有發生變化。
-
有負數時,不用基數排序來進行排序
如果要支援負數可以參考 中文維基百科
由於上述演算法使用的按位比較,並未考慮負數,直接使用,將導致陣列越界。
改造支援負數的核心思想是:將負數取絕對值,然後再反轉成負數。