排序充斥着我們的生活,比如站隊、排隊買票、考試排名、公司業績排名、將電子郵件按時間排序、QQ 好友列表中的會員紅名靠前,等等。
這裏先舉個例子,通過這個例子讓我們接觸第 1 個算法。
在某個期末考試中,老師要把大家的分數排序,比如有 5 個學生,分別考 5、9、5、1、6 分(滿分 10 分),從大到小排序應該是 9、6、5、5、1,大家有沒有辦法寫一段程序隨機讀取 5 個數,然後對它們排序呢?
看到這個問題,我們用 5 分鐘想一下該怎麼辦。辦法當然很多,這裏使用桶排序的思想來處理。
我們找到 11 個桶,分別編號爲 0-10,對應 0-10 分,如圖 1 所示。
着我們把這些分數按照桶的編號放入桶中,如圖 2 所示。
接着我們從最大編號的桶到最小編號的桶依次輸出每個桶中的分數,分別是 9、6、5、5、1 了。是不是很輕鬆地完成排序了呢?這就是桶排序的思想。
什麼是桶排序
桶排序
,也叫作箱排序
,是一個排序算法,也是所有排序算法中最快、最簡單的排序算法。其中的思想是我們首先需要知道所有待排序元素的範圍,然後需要有在這個範圍內的同樣數量的桶,接着把元素放到對應的桶中,最後按順序輸出。
這實際上是簡易版的桶排序,我們想象一下,如果考試分數的範圍是 0~100 萬該怎麼辦?弄 100 萬個桶嗎?
實際上在這種情況下,一個桶並不總是放同一個元素,在很多時候一個桶裏可能會放多個元素,這是不是與散列表有點相似呢?其實真正的桶排序和散列表有一樣的原理。
除了對一個桶內的元素做鏈表存儲,我們也有可能對每個桶中的元素繼續使用其他排序算法進行排序,所以更多時候,桶排序會結合其他排序算法一起使用。
桶排序的實現
簡易版實現
我們怎麼在代碼中實現桶排序呢?其實很簡單,使用數組就好了。比如有 11 個桶,我們只需要聲明一個長度爲 11 的數組,然後每把一個元素往桶中放時,就把數組指定位置的值加 1,最終倒序輸出數組的下標,數組每個位置的值爲幾就輸出幾次下標,這樣就可以實現桶排序了。
下面我們一起看看簡易版桶排序的代碼。
public class BucketSort01 {
public static void main(String[] args) {
int[] array = {5, 9, 1, 9, 5, 3, 7, 6, 1};// 待排序數組
int[] buckets = new int[11];
sort(array, buckets);
print(buckets);
}
/** 從小到大排序 */
public static void sort(int array[], int buckets[]) {
for (int i = 0; i < array.length; i++) {
buckets[array[i]]++;
}
}
/** 從小到大排序 */
public static void print(int buckets[]) {
// 順序輸出數據
for (int i = 0; i < buckets.length; i++) {
// 元素中值爲幾,說明有多少個相同值的元素,則輸出幾遍
for (int j = 0; j < buckets[i]; j++) {
System.out.print(i + " ");
}
}
}
}
正式版實現
一個桶並不總是放同一個元素,在很多時候一個桶裏可能會放多個元素。
public class BucketSort03 {
public static void main(String[] args) {
int[] array = {50, 9, 1, 9, 53, 33, 27, 6, 1};// 待排序數組
sort(array);
print(array);
}
/** 從小到大排序 */
public static void sort(int[] array) {
// 確定元素的最值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < array.length; i++) {
max = Math.max(max, array[i]);
min = Math.min(min, array[i]);
}
// 桶數:(max - min) / array.length的結果爲數組大小的倍數(最大倍數),以倍數作爲桶數
int bucketNum = (max - min) / array.length + 1;
// 初始化桶
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<Integer>());
}
// 將每個元素放入桶
for (int i = 0; i < array.length; i++) {
// 計算每個(array[i] - min)是數組大小的多少倍,看看放入哪個桶裏
int num = (array[i] - min) / (array.length);
bucketArr.get(num).add(array[i]);
}
// 對每個桶進行排序
for (int i = 0; i < bucketArr.size(); i++) {
Collections.sort(bucketArr.get(i));
}
// 合併數據
int j = 0;
for (ArrayList<Integer> tempList : bucketArr) {
for (int i : tempList) {
array[j++] = i;
}
}
}
/** 打印數組 */
public static void print(int array[]) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
高級版實現
基於基數排序實現的桶排序。參考:基數排序算法原理及實現和優化
public class BucketSort04 {
public static void main(String[] args) {
int[] array = {51, 944, 1, 9, 57, 366, 79, 6, 1, 345};// 待排序數組
sort(array);
System.out.println("最終排好序的數據:");
print(array);
}
/**
* 從小到大排序
*/
public static void sort(int data[]) {
int n = data.length;
// 使用數組來模擬鏈表(當然犧牲了部分的空間,但是操作卻是簡單了很多,穩定性也大大提高了)
// 十個桶。建立一個二維數組,行向量的下標0—9代表了10個桶,每個行形成的一維數組則是桶的空間
int bask[][] = new int[10][n];
// 用來計算每個桶使用的容量
int index[] = new int[10];
// 計算最大的數有多少位。比如:5978,有4位
int max = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int k = (data[i] + "").length();
max = max > k ? max : k;
}
String str;
// 循環內將所有數據補齊,長度都爲 max 。第一輪 i 代表個位,第二輪 i 代表十位。。。
// 按照 個、十、百、千...的位置來計算
// 第一輪將10以內的數據排好序,第二輪將100以內的數據排好序......
for (int i = max - 1; i >= 0; i--) {
System.out.println("第" + (max - i) + "輪補齊後的數據:");
// 所有的數字都循環一遍
for (int j = 0; j < n; j++) {
str = "";
// 按照 max 將所有的數據補齊,位數不足的前面補零
if (Integer.toString(data[j]).length() < max) {
for (int k = 0; k < max - Integer.toString(data[j]).length(); k++)
str += "0";
}
str += Integer.toString(data[j]);
System.out.printf("%5s", str);
// index[str.charAt(i) - '0']用於第二層循環計算每個桶使用的容量,第二層循環結束後會將index[str.charAt(i) - '0']都初始化爲零
// 第一輪取 str 的個位(str.charAt(i--)),放在第(str.charAt(i--) - '0')個桶的第(index[str.charAt(i) - '0']++)個位置
// 第二輪取 str 的十位(str.charAt(i--)),放在第(str.charAt(i--) - '0')個桶的第(index[str.charAt(i) - '0']++)個位置
// .......
bask[str.charAt(i) - '0'][index[str.charAt(i) - '0']++] = data[j];
}
// 將桶內的數據重新放入data數組內
int pos = 0;
for (int j = 0; j < 10; j++) {
// 第j個桶內有index[j]個數據
for (int k = 0; k < index[j]; k++) {
data[pos++] = bask[j][k];
}
}
System.out.println();
System.out.println("第" + (max - i) + "輪index內的數據:");
print(index);
System.out.println("第" + (max - i) + "輪桶內的數據:");
print(bask);
System.out.println("第" + (max - i) + "輪結束後data內的數據:");
print(data);
System.out.println();
// 將index[x]歸零
for (int x = 0; x < 10; x++) index[x] = 0;
}
}
public static void print(int array[][]) {
for (int j = 0; j < array.length; j++) {
for (int k = 0; k < array[j].length; k++) {
System.out.printf("%5d", array[j][k]);
}
System.out.println();
}
}
public static void print(int array[]) {
for (int j = 0; j < array.length; j++) {
System.out.printf("%5d", array[j]);
}
System.out.println();
}
}
控制檯輸出:
第一輪將10以內的數據排好序,第二輪將100以內的數據排好序…
第1輪補齊後的數據:
051 944 001 009 057 366 079 006 001 345
第1輪index內的數據:
0 3 0 0 1 1 2 1 0 2
第1輪桶內的數據:
0 0 0 0 0 0 0 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 0 0 0 0 0 0 0 0 0
345 0 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
57 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
9 79 0 0 0 0 0 0 0 0
第1輪結束後data內的數據:
51 1 1 944 345 366 6 57 9 79
第2輪補齊後的數據:
051 001 001 944 345 366 006 057 009 079
第2輪index內的數據:
4 0 0 0 2 2 1 1 0 0
第2輪桶內的數據:
1 1 6 9 0 0 0 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 345 0 0 0 0 0 0 0 0
51 57 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
79 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
9 79 0 0 0 0 0 0 0 0
第2輪結束後data內的數據:
1 1 6 9 944 345 51 57 366 79
第3輪補齊後的數據:
001 001 006 009 944 345 051 057 366 079
第3輪index內的數據:
7 0 0 2 0 0 0 0 0 1
第3輪桶內的數據:
1 1 6 9 51 57 79 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
345 366 0 0 0 0 0 0 0 0
944 345 0 0 0 0 0 0 0 0
51 57 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
79 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 79 0 0 0 0 0 0 0 0
第3輪結束後data內的數據:
1 1 6 9 51 57 79 345 366 944
最終排好序的數據:
1 1 6 9 51 57 79 345 366 944
桶排序的時間複雜度
對於N個待排數據,M個桶,平均每個桶[N/M]個數據的桶排序平均時間複雜度爲:
O(N)+O(M*(N/M)*log(N/M)) = O(N+N*(logN-logM)) = O(N+N*logN-N*logM)
當N=M時,即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。
總結:桶排序的平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。當然桶排序的空間複雜度爲O(N+M),如果輸入數據非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的
。
通過上面的性能分析,我們可以知道桶排序的特點,那就是速度快、簡單,但是也有相應的弱點,那就是空間利用率低,如果數據跨度過大,則空間可能無法承受,或者說這些元素並不適合使用桶排序算法。
桶排序的適用場景
桶排序的適用場景非常明瞭,那就是在數據分佈相對比較均勻或者數據跨度範圍並不是很大時,排序的速度還是相當快且簡單的。
但是當數據跨度過大時,這個空間消耗就會很大;如果數值的範圍特別大,那麼對空間消耗的代價肯定也是不切實際的,所以這個算法還有一定的侷限性。同樣,由於時間複雜度爲 O(n+m),如果 m 比 n 大太多,則從時間上來說,性能也並不是很好。
但是實際上在使用桶排序的過程中,我們會使用類似散列表的方式去實現,這時的空間利用率會高很多,同時時間複雜度會有一定的提升,但是效率還不錯。
我們在開發過程中,除了對一些要求特別高並且數據分佈較爲均勻的情況使用桶排序,還是很少使用桶排序的,所以即使桶排序很簡單、很快,我們也很少使用它。
桶排序更多地被用於一些特定的環境,比如數據範圍較爲侷限或者有一些特定的要求,比如需要通過哈希映射快速獲取某些值、需要統計每個數的數量。但是這一切都需要確認數據的範圍,如果範圍太大,就需要巧妙地解決這個問題或者使用其他算法了。