計數排序、桶排序和基數排序
轉載自:http://blog.csdn.net/quietwave/article/details/8008572
計數排序
當輸入的元素是 n 個 0 到 k 之間的整數時,它的執行時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。
由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量時間和記憶體。例如:計數排序是用來排序0到100之間的數字的最好的演算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的演算法來排序資料範圍很大的陣列。
演算法的步驟如下:
- 找出待排序的陣列中最大和最小的元素
- 統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
- 反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1
貼上程式碼:
- #include <stdio.h>
- #include <stdlib.h>
- #include <time.h>
- //對於排序的關鍵字範圍,一定是0-99
- #define NUM_RANGE (100)
- void print_arr(int *arr, int n)
- {
- int i;
-
for(i=0; i
- if(!i){
- printf("%d", arr[i]);
- }else{
- printf(" %d", arr[i]);
- }
- }
- printf("\n");
- }
- /*
- 演算法的步驟如下:
- 1.找出待排序的陣列中最大和最小的元素
- 2.統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項
-
3.對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
- 4.反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1
- */
- void counting_sort(int *ini_arr, int *sorted_arr, int n)
- {
- int *count_arr = (int *)malloc(sizeof(int) * NUM_RANGE);
- int i, j, k;
- //統計陣列中,每個元素出現的次數
- for(k=0; k<NUM_RANGE; k++){
- count_arr[k] = 0;
- }
- for(i=0; i<n; i++){
- count_arr[ini_arr[i]]++;
- }
- for(k=1; k<NUM_RANGE; k++){
- count_arr[k] += count_arr[k-1];
- }
- for(j=n-1 ; j>=0; j--){
- int elem = ini_arr[j];
- int index = count_arr[elem]-1;
- sorted_arr[index] = elem;
- count_arr[elem]--;
- }
- free(count_arr);
- }
- int main(int argc, char* argv[])
- {
- int n;
- if(argc <2){
- n = 10;
- }else{
- n = atoi(argv[1]);
- }
- int i;
- int *arr = (int *)malloc(sizeof(int) * n);
- int *sorted_arr = (int *)malloc(sizeof(int) *n);
- srand(time(0));
- for(i=0; i<n; i++){
- arr[i] = rand() % NUM_RANGE;
- }
- printf("ini_array: ");
- print_arr(arr, n);
- counting_sort(arr, sorted_arr, n);
- printf("sorted_array: ");
- print_arr(sorted_arr, n);
- free(arr);
- free(sorted_arr);
- return 0;
- }
桶排序的基本思想
假設有一組長度為N的待排關鍵字序列K[1....n]。首先將這個序列劃分成M個的子區間(桶) 。然後基於某種對映函式 ,將待排序列的關鍵字k對映到第i個桶中(即桶陣列B的下標 i) ,那麼該關鍵字k就作為B[i]中的元素(每個桶B[i]都是一組大小為N/M的序列)。接著對每個桶B[i]中的所有元素進行比較排序(可以使用快排)。然後依次列舉輸出B[0]....B[M]中的全部內容即是一個有序序列。
假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。這些資料全部在1—100之間。因此我們定製10個桶,然後確定對映函式f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將所有關鍵字全部堆入桶中,並在每個非空的桶中進行快速排序。
桶排序代價分析
桶排序利用函式的對映關係,減少了幾乎所有的比較工作。實際上,桶排序的f(k)值的計算,其作用就相當於快排中劃分,已經把大量資料分割成了基本有序的資料塊(桶)。然後只需要對桶中的少量資料做先進的比較排序即可。
對N個關鍵字進行桶排序的時間複雜度分為兩個部分:
(1) 迴圈計算每個關鍵字的桶對映函式,這個時間複雜度是O(N)。
(2) 利用先進的比較排序演算法對每個桶內的所有資料進行排序,其時間複雜度為 ∑ O(Ni*logNi) 。其中Ni 為第i個桶的資料量。
很顯然,第(2)部分是桶排序效能好壞的決定因素。儘量減少桶內資料的數量是提高效率的唯一辦法(因為基於比較排序的最好平均時間複雜度只能達到O(N*logN)了)。因此,我們需要儘量做到下面兩點:
(1) 對映函式f(k)能夠將N個數據平均的分配到M個桶中,這樣每個桶就有[N/M]個數據量。
(2) 儘量的增大桶的數量。極限情況下每個桶只能得到一個數據,這樣就完全避開了桶內資料的“比較”排序操作。 當然,做到這一點很不容易,資料量巨大的情況下,f(k)函式會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。
對於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(logN)。比如折半查詢、平衡二叉樹、紅黑樹等。但是Hash表卻有O(C)線性級別的查詢效率(不衝突情況下查詢效率達到O(1))。大家好好體會一下:Hash表的思想和桶排序是不是有一曲同工之妙呢?
基數排序
上面的問題是多關鍵字的排序,但單關鍵字也仍然可以使用這種方式。
比如字串“abcd” “aesc” "dwsc" "rews"就可以把每個字元看成一個關鍵字。另外還有整數 425、321、235、432也可以每個位上的數字為一個關鍵字。
基數排序的思想就是將待排資料中的每組關鍵字依次進行桶分配。比如下面的待排序列:
278、109、063、930、589、184、505、269、008、083
我們將每個數值的個位,十位,百位分成三個關鍵字: 278 -> k1(個位)=8 ,k2(十位)=7 ,k3=(百位)=2。
然後從最低位個位開始(從最次關鍵字開始),對所有資料的k1關鍵字進行桶分配(因為,每個數字都是 0-9的,因此桶大小為10),再依次輸出桶中的資料得到下面的序列。
930、063、083、184、505、278、008、109、589、269
再對上面的序列接著進行鍼對k2的桶分配,輸出序列為:
505、008、109、930、063、269、278、083、184、589
最後針對k3的桶分配,輸出序列為:
008、063、083、109、184、269、278、505、589、930
效能分析
很明顯,基數排序的效能比桶排序要略差。每一次關鍵字的桶分配都需要O(N)的時間複雜度,而且分配之後得到新的關鍵字序列又需要O(N)的時間複雜度。假如待排資料可以分為d個關鍵字,則基數排序的時間複雜度將是O(d*2N) ,當然d要遠遠小於N,因此基本上還是線性級別的。基數排序的空間複雜度為O(N+M),其中M為桶的數量。一般來說N>>M,因此額外空間需要大概N個左右。
但是,對比桶排序,基數排序每次需要的桶的數量並不多。而且基數排序幾乎不需要任何“比較”操作,而桶排序在桶相對較少的情況下,桶內多個數據必須進行基於比較操作的排序。因此,在實際應用中,基數排序的應用範圍更加廣泛。