前言
基數排序,屬於桶排序的一種,是一種典型的空間換取時間的 穩定式排序。
一、基數排序(桶排序)介紹
-
基數排序(radix sort)屬於
“分配式排序”
(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用 -
基數排序法是屬於
穩定性的排序
,基數排序法的是效率高的穩定性排序法 -
基數排序(Radix Sort)是
桶排序的擴展
-
基數排序是1887年赫爾曼·何樂禮發明的。它是這樣實現的:將整數按位數切割成不同的數字,然後按每個位數分別比較。
二、基數排序基本思想
- 將所有待比較數值統一爲同樣的數位長度,數位較短的數前面補零。
- 然後,從最低位開始,依次進行一次排序。
- 這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。
- 這樣說明,比較難理解,下面我們看一個圖文解釋,理解基數排序的步驟
三、圖文解釋
將數組 {53, 3, 542, 748, 14, 214} 使用基數排序, 進行升序排序。
數組的初始狀態 array = {53, 3, 542, 748, 14, 214}
3.1 第 1 次排序
- 第1輪排序規則:
- 將
每個元素的個位數取出,然後看這個數應該放在哪個對應的桶
(一個一維數組) - 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
- 排序結果:
數組的第1輪排序結果 array = {542, 53, 3, 14, 214, 748}
3.2 第 2 次排序
-
第2輪排序規則:
(1)將每個元素的十位數取出,然後看這個數應該放在哪個對應的桶
(一個一維數組)
(2) 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組) -
排序結果:
數組的第2輪排序結果 array = {3, 14, 214, 542, 748, 53}
3.3 第 3 次排序
- 第3輪排序規則:
(1) 將每個元素百位數取出,然後看這個數應該放在哪個對應的桶(一個一維數組)
(2) 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組) - 排序結果:
數組的第3輪排序結果 array = {3, 14, 53, 214, 542, 748}
3.4 結果
到了這裏,是最終的結果。array = {3, 14, 53, 214, 542, 748} ,
所以,排序幾次,需要找到最大數的是幾位數。
四、代碼實現
- deductionRadixSort(int[] array); 爲學習推導的過程
- radixSort(int[] array);據下面的推導過程,我們得到的最終的基數排序代碼
- testTime();測試速度的方法;相當之快,這也正是 空間換取時間的原因所在,
8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 。
當爲8000萬數據時,會報錯:java.lang.OutOfMemoryError: Java heap space,內存溢出錯誤,因爲 80000000 * 11(數組) *4(一個int 4個字節) /1024(k) /1024(m)/1024(g) = 3.3G ,會用到 3.3G內存,所以會內存溢出
package com.feng.ch09_sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/*
* 基數排序(穩定式排序)
* 是空間換時間的經典算法
*
* 速度: 這也正是 空間換取時間的原因所在
* 相當之快:8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 ,
* 當爲8000萬數據時,會報錯:java.lang.OutOfMemoryError: Java heap space,內存溢出錯誤,
* 因爲 80000000 * 11(數組) *4(一個int 4個字節) /1024(k) /1024(m)/1024(g) = 3.3G ,會用到 3.3G內存,所以會內存溢出
* */
public class S7_RadixSort {
public static void main(String[] args) {
int array[] = {53, 3, 542, 748, 14, 214};
System.out.println("初始數組:");
System.out.println(Arrays.toString(array));
// deductionRadixSort(array); // 測試推導方法
radixSort(array); // 測試推導後 合成的方法
System.out.println();
System.out.println("排序後的數組:");
System.out.println(Arrays.toString(array));
// 測試 80000 個數據排序 所用的時間
System.out.println();
System.out.println("測試 8000000 個數據 採用基數排序 所用的時間:");
testTime(); // 8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 , 8000萬數據:
}
/*
* 測試一下 歸併排序的速度, 給 80000 個數據,測試一下
* */
public static void testTime() {
// 創建一個 80000個的隨機的數組
int array2[] = new int[8000000];
for (int i = 0; i < 8000000; i++) {
array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
}
// System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長
long start = System.currentTimeMillis(); //返回以毫秒爲單位的當前時間
System.out.println("long start:" + start);
Date date = new Date(start); // 上面的也可以不要,但是我想測試
System.out.println("date:" + date);
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
System.out.println("排序前的時間是=" + format.format(date));
radixSort(array2);
System.out.println();
long end = System.currentTimeMillis();
Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
System.out.println("排序後的時間是=" + format.format(date2));
System.out.println("共耗時" + (end - start) + "毫秒");
System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
}
/*
* 根據下面的推導過程,我們可以得到最終的基數排序代碼
* */
public static void radixSort(int[] array) {
//1、得到數組中最大的數的位數
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
//2、得到最大數是幾位數
int maxLength = (max + "").length();
/*
* 定義一個二維數組,表示 10 個桶,每個桶就是一個一維數組
* 說明
* 1、二維數組包含 10 個一維數組
* 2、爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定位 array.length
* 3、很明顯,基數排序是使用空間換時間的經典算法
* */
int[][] bucket = new int[10][array.length];
/*
* 爲了記錄每個桶中,實際存放了多少個數據,我們定義一個一維數組來記錄各個桶的每次放入的數據的個數
* 可以這樣理解:
* 比如:bucketElementCounts[0] , 記錄的就是 bucket[0] 桶的放入數據個數
* */
int[] bucketElementCounts = new int[10];
// 這裏使用循環將代碼處理
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
/*
* 每一輪(針對每個元素的 對應位 進行排序處理)第一次是個位,第二次是十位,第三次是百位,第四次是千位。。。。
* 爲了解決這個問題,在上面的 for 循環中,添加了一個 變量 n ,每次 * 10,因爲下面需要取出相應的 個位、十位、百位。。。
* 注意點:
* 每輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
* */
for (int j = 0; j < array.length; j++) {
// 取出每個元素的 個位 的值
int digitOfElement = (array[j] / n) % 10; // =3,第三個桶 //
// 放入到對應的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j]; // 我覺得第二個 值 有問題
bucketElementCounts[digitOfElement]++;
}
//按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
int index = 0;
// 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length bucket.length 都爲 10
// 如果桶中有數據,我們才放到原數據
if (bucketElementCounts[k] != 0) { // bucket[i] !=0
// 循環該桶,即第 k 個桶(即第K 個數組中),放入 ;bucketElementCounts[k]: 爲桶的幾個數據
for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
// 取出元素放入到 array
array[index] = bucket[k][j];
index++;
}
}
// 第 i+1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
bucketElementCounts[k] = 0;
}
// System.out.println("第 " + (i + 1) + " 輪,對" + n + "位數的排序處理 array = " + Arrays.toString(array)); //
}
}
/*
* 基數排序
* 使用逐步推導的方式
* */
public static void deductionRadixSort(int[] array) {
/*
* 定義一個二維數組,表示 10 個桶,每個桶就是一個一維數組
* 說明
* 1、二維數組包含 10 個一維數組
* 2、爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定位 array.length
* 3、很明顯,基數排序是使用空間換時間的經典算法
* */
int[][] bucket = new int[10][array.length];
/*
* 爲了記錄每個桶中,實際存放了多少個數據,我們定義一個一維數組來記錄各個桶的每次放入的數據的個數
* 可以這樣理解:
* 比如:bucketElementCounts[0] , 記錄的就是 bucket[0] 桶的放入數據個數
* */
int[] bucketElementCounts = new int[10];
/*
* 第 1 輪(針對每個元素的個位進行排序處理)
* 注意點:
* 第 1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
* */
for (int j = 0; j < array.length; j++) {
// 取出每個元素的 個位 的值
int digitOfElement = (array[j] / 1) % 10; // =3,第三個桶 //
// 放入到對應的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j]; // 我覺得第二個 值 有問題
bucketElementCounts[digitOfElement]++;
}
//按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
int index = 0;
// 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length bucket.length 都爲 10
// 如果桶中有數據,我們才放到原數據
if (bucketElementCounts[k] != 0) { // bucket[i] !=0
// 循環該桶,即第 k 個桶(即第K 個數組中),放入 ;bucketElementCounts[k]: 爲桶的幾個數據
for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
// 取出元素放入到 array
array[index] = bucket[k][j];
index++;
}
}
// 第 1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
bucketElementCounts[k] = 0;
}
System.out.println("第 1 輪,對個位數的排序處理 array = " + Arrays.toString(array)); //[542, 53, 3, 14, 214, 748]
/*
* 第 2 輪(針對每個元素的個位進行排序處理)
* 注意點:
* 取出每個元素的 十位 的值:int digitOfElement = (array[j]/10) % 10
* */
for (int j = 0; j < array.length; j++) {
// 取出每個元素的 十位 的值
int digitOfElement = (array[j] / 10) % 10; // =3,第三個桶 //
// 放入到對應的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j]; // 我覺得第二個 值 有問題
bucketElementCounts[digitOfElement]++;
}
//按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
index = 0;
// 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length bucket.length 都爲 10
// 如果桶中有數據,我們才放到原數據
if (bucketElementCounts[k] != 0) { // bucket[i] !=0
// 循環該桶,即第 k 個桶(即第K 個數組中),放入 ;bucketElementCounts[k]: 爲桶的幾個數據
for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
// 取出元素放入到 array
array[index] = bucket[k][j];
index++;
}
}
// 第 2 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
bucketElementCounts[k] = 0;
}
System.out.println("第 2 輪,對十位數的排序處理 array = " + Arrays.toString(array)); // [3, 14, 214, 542, 748, 53]
/*
* 第 3 輪(針對每個元素的個位進行排序處理)
* 注意點:
* 取出每個元素的 百位 的值:int digitOfElement = (array[j]/100) % 10
* */
for (int j = 0; j < array.length; j++) {
// 取出每個元素的 百位 的值
int digitOfElement = (array[j] / 100) % 10; // =3,第三個桶 //
// 放入到對應的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j]; // 我覺得第二個 值 有問題
bucketElementCounts[digitOfElement]++;
}
//按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
index = 0;
// 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length bucket.length 都爲 10
// 如果桶中有數據,我們才放到原數據
if (bucketElementCounts[k] != 0) { // bucket[i] !=0
// 循環該桶,即第 k 個桶(即第K 個數組中),放入 ;bucketElementCounts[k]: 爲桶的幾個數據
for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
// 取出元素放入到 array
array[index] = bucket[k][j];
index++;
}
}
// 第 3 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
bucketElementCounts[k] = 0;
}
System.out.println("第 3 輪,對百位數的排序處理 array = " + Arrays.toString(array)); // [3, 14, 214, 542, 748, 53]
}
}
五、基數排序說明
- 基數排序是對傳統桶排序的擴展,速度很快.
- 基數排序是經典的空間換時間的方式,佔用內存很大, 當對海量數據排序時,容易造成 OutOfMemoryError 。
- 基數排序時穩定的。[注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,
即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的]
- 有負數的數組,我們不用基數排序來進行排序, 如果要支持負數,參考: https://code.i-harness.com/zh-CN/q/e98fa9