排序演算法(三):計數排序與桶排序
插入排序、堆排序、歸併排序等排序方法,在排序的最終結果中,各個元素的次序依賴於他們之間的比較,我們把這一類的排序演算法稱為比較排序。在最壞情況下,任何比較排序演算法都要經過 Omega(nlgn)次比較。因此堆排序和歸併排序都是漸近最優的比較排序演算法。
計數排序、基數排序和桶排序因為不採用比較排序方法,因此可以打破其下界。本文主要介紹計數排序和桶排序。
一、計數排序
計數排序假設n個輸入元素中的每一個都是在0到k區間內的一個整數,其中k為某個整數。當k=O(n)時,計數排序的執行時間為\Seta(n)。
1. 計數排序的基本思想
對於一個輸入陣列中的一個元素x,只要我們知道了這個陣列中比x小的元素的個數,那麼我們就可以直接把x放到(x+1)的位置上。這就是計數排序的基本思想。
基於這個思想,計數排序的一個主要問題就是如何統計陣列中元素的個數。再加上輸入陣列中的元素都是0-k區間的一個整數
下面給出統計陣列元素都是0-k區間的整數的陣列中各個元素個數的方法。
/**
* 統計陣列元素都是0-k區間的整數的陣列中各個元素個數
* @param src
* @param k 元素分佈區間
*/
public static int[] getArrayCount(int[] src,int k) {
int c[] = new int[k];
for(int i : src)
c[i]++;
return c;
}
2.計數排序實現
public class CountSort{
public static void main(String args[]) {
int k = 10;
int test[] = com.sunpro.java.RandomGenerator.randGenerator(10,k);
com.sunpro.java.Print.printArray(test);
countSort(test, k);
com.sunpro.java.Print.printArray(test);
}
public static void countSort(int[] A , int k){
//初始化count陣列
int[] count = new int[k];
//為輸入陣列中每個元素計數
for(int i : A)
count[i]++;
//計算各個數之前元素的總和
for(int i = 1; i < k; i++)
count[i] = count[i] + count[i-1];
//初始化一個新的陣列存放排序後的元素
int[] B = new int[A.length];
for(int j = A.length-1; j >= 0; j--){
//把A[j]放到對應的位置
B[count[A[j]]-1] = A[j];
//計數器減一
count[A[j]]--;
}
System.arraycopy(B,0, A, 0,A.length);
}
}
計數排序的一個重要性質是穩定性,具有相同值的元素在輸出陣列中的相對次序與輸入陣列陣列中的次序相同。這種穩定性在進行排序的資料電郵衛星資料的時候比較重要。
二、桶排序
桶排序(bucket sort)假設輸入資料服從均勻分佈。平均情況下他的時間代價是O(n)。計數排序假設輸入資料分佈於一個小區間的整數,而桶排序則假設輸入是一個隨機過程產生的,該過程將元素均勻獨立地分佈於[0,1)區間上。
1.桶排序的基本思想
桶排序將[0,1)區間劃分為n個相同的大小的子區間,這些子區間被稱為桶。然後將n個輸入元素分別放入各自的桶中。因為輸入時均勻獨立的,所以一般不會有很多數同時落在一個桶中的情況。這樣,我們想對各個桶中的資料進行排序,然後遍歷每個桶,按照次序把各個桶中的元素列出來即可。
一個桶排序的示例如圖:
簡單來說就是把陣列 arr 劃分為n個大小相同子區間(桶),每個子區間各自排序,最後合併。這樣說是不是和分治法有點像了 啊!因為分治法就是分解 —— 解決 ——合併這樣的套路。我認為這樣想沒毛病。可以理解為桶排序是一種特殊的分治法,特殊的地方主要體現在前兩部分(這樣說不知道對不對~)。具體來說如下:
分解部分,採用了計數排序類似的思想,通過分解,雖然沒有比較,但是將資料基本按照大小劃分了幾個區間。針對輸入資料均勻分佈的特點,因此將資料分佈的區間可以均勻分為n個子區間。那麼就有
max - min = n * width; (1)
其中,max,min 是輸入資料的最大值最小值,n是子區間個數,width是子區間寬度。
這樣劃分後,每個資料x對應的桶的編號(0-n-1)就是;
index = (x - min) / width = (x - min) / (max - min) * n;(2)
這樣,當我們取n=Array.length時,相應的每個資料的存放的桶的編號也就確定了:
index = (x - min) / (max - min) * Array.length
如果我們取n= (max-min)/Array.length 時,就有:
index = (x - min) / (max - min) * (max-min) / Array.length = (x - min) / Array.length;
這樣形式上看起來比較簡單,在實際程式設計中也有一定的方便性。在效能上是不是有什麼優缺點還沒有什麼發現。
2. 桶排序的實現
第一種分組方案:
public static void bucketSort(int[] A) {
//1. 構造桶
//1.1 確定桶的個數n
int n = A.length;
//1.2 宣告並初始化一個List,存放連結串列;
List<ArrayList<Integer>> Blist = new ArrayList<>(n);
for(int i = 0; i < n; i++)
Blist.add(new ArrayList<Integer>(5));
//2.將陣列中的元素放到桶中
//2.1 確定元素的最值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int a : A){
max = Math.max(max, a);
min = Math.min(min, a);
}
//2.2 確定每個元素放入桶的編號並放進去
for(int i : A){
//2.2.1 確定桶的編號
int len = A.length;
//加1是為了保證inde< A.length,防止程式丟擲IndexOutofBoundsEx;
int index = (int)((i-min) / (max-min+1.0) * A.length);
//2.2.2 放入對應的桶中
Blist.get(index).add(i);
}
//3.桶內排序
for(int i = 0; i < Blist.size(); i++){
java.util.Collections.sort(Blist.get(i));
}
//4.合併資料
int j = 0;
for(ArrayList<Integer> arr : Blist){
for(int i : arr){
A[j++] = i;
}
}
}
第二種分組方案:
public static void bucketSort(int[] arr){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
//桶數
int bucketNum = (max - min) / arr.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 < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
//對每個桶進行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
3. 桶排序的運行復雜度分析
根據程式我們容易得到,最壞情況下,桶排序的時間複雜度為:
平均情況下:
桶排序的期望時間為:
這些具體的推導可以參考《演算法導論》。總而言之一句話:
只要所有桶的大小的平方和與元素總數成線性關係,桶排序就能線上性時間能完成。
那麼怎麼才能算“所有桶的大小的平方和與元素總數成線性關係”呢?正好,我們通過上面給出兩個方案來對比一下:
public static void main(String args[]) {
//總數小,範圍小
int test1[] = com.sunpro.java.RandomGenerator.randGenerator(10,10);
//總數小,範圍大
int test2[] = com.sunpro.java.RandomGenerator.randGenerator(10,1000);
//總數大,範圍小
int test3[] = com.sunpro.java.RandomGenerator.randGenerator(100,10);
//總數大,範圍大
int test4[] = com.sunpro.java.RandomGenerator.randGenerator(100,1000);
//int test[] = {9, 99, 37, 41, 34, 33, 69, 92, 2, 18};
//com.sunpro.java.Print.printArray(test);
System.out.println("總數小,範圍小:方案二:");
bucketSort2(test1);
System.out.println("方案一:");
bucketSort(test1);
System.out.println("總數小,範圍大:方案二:");
bucketSort2(test2);
System.out.println("方案一:");
bucketSort(test2);
System.out.println("總數大,範圍小:方案二:");
bucketSort2(test3);
System.out.println("方案一:");
bucketSort(test3);
System.out.println("總數大,範圍大:方案二:");
bucketSort2(test4);
System.out.println("方案一:");
bucketSort(test4);
//com.sunpro.java.Print.printArray(test);
}
執行結果:
桶排序>java BucketSort
總數小,範圍小:
方案二:
[[0, 1, 1, 2, 4, 5, 6, 8, 8, 9]]
方案一:
[[0], [1, 1], [2], [], [4], [5], [6], [], [8, 8], [9]]
總數小,範圍大:
方案二:
[[2], [], [], [], [], [], [], [], [], [], [], [117], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [320], [], [], [342]
, [], [], [], [], [], [408], [418], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [663], [], [684],
[], [], [], [], [], [], [756], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [993]]
方案一:
[[2], [117], [], [320, 342], [408, 418], [], [663, 684], [756], [], [993]]
總數大,範圍小:
方案二:
[[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5,
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
9]]
方案一:
[[0, 0, 0, 0, 0, 0], [], [], [], [], [], [], [], [], [], [1, 1, 1, 1, 1, 1, 1], [], [], [], [], [], [], [], [], [], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [
], [], [], [], [], [], [], [], [], [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [], [], [], [], [], [], [], [], [], [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], []
, [], [], [], [], [], [], [], [], [5, 5, 5, 5, 5, 5, 5, 5, 5], [], [], [], [], [], [], [], [], [], [6, 6, 6, 6, 6, 6, 6, 6, 6, 6], [], [], [], [], []
, [], [], [], [], [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7], [], [], [], [], [], [], [], [], [], [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8], [], [], [], [],
[], [], [], [], [], [9, 9, 9, 9, 9, 9, 9, 9, 9], [], [], [], [], [], [], [], [], []]
總數大,範圍大:
方案二:
[[1, 11, 18, 30, 35, 58, 77, 100], [112, 128, 134, 139, 140, 141, 146, 159, 159, 184, 185, 193], [202, 216, 227, 236, 238, 241, 256, 257, 273, 300],
[302, 308, 309, 310, 342, 345, 355, 358, 363, 364, 391], [406, 410, 425, 431, 442, 471, 473, 482], [502, 538, 566, 573, 581, 588, 598, 599], [602, 60
9, 612, 646, 646, 660, 663, 663, 675, 684], [705, 708, 716, 719, 761, 761, 762, 773, 789, 793, 797], [801, 812, 823, 826, 845, 852, 853, 855, 874, 87
7, 880, 880, 882, 900], [904, 923, 932, 957, 969, 977, 985, 997]]
方案一:
[[1], [11, 18], [30], [35], [], [58], [], [77], [], [100], [], [112], [128], [134, 139, 140], [141, 146], [159, 159], [], [], [184, 185], [193], [202
], [216], [227], [236, 238], [241], [256, 257], [], [273], [], [300], [302, 308, 309, 310], [], [], [], [342, 345], [355, 358], [363, 364], [], [], [
391], [406], [410], [425], [431], [442], [], [], [471, 473], [482], [], [502], [], [], [538], [], [], [566], [573], [581, 588], [598, 599], [602, 609
], [612], [], [], [646, 646], [], [660, 663, 663], [675], [684], [], [705, 708], [716], [719], [], [], [], [761, 761, 762], [773], [], [789, 793, 797
], [801], [812], [823, 826], [], [845], [852, 853, 855], [], [874, 877], [880, 880, 882], [], [900, 904], [], [923], [932], [], [957], [], [969, 977]
, [985], [997]]
好吧,沒有對比就沒有傷害!通過這四種情況下的輸出,我們可以得到直觀的結論:
對小範圍情況,第二種方案肯定都不是線性的。對於大範圍情況,第二種方案的也基本不是線性的。
而對於第一種方案,基本都可以保持線性,但是在“總數大,範圍小”的情況下的線性也不好。實際上這種情況比較適合計數排序。
參考文獻:
《演算法導論》Thomas