1. 程式人生 > >計數排序、桶排序和基數排序

計數排序、桶排序和基數排序

轉載自:http://blog.csdn.net/quietwave/article/details/8008572

計數排序

當輸入的元素是 n 個 0 到 k 之間的整數時,它的執行時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。

由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量時間和記憶體。例如:計數排序是用來排序0到100之間的數字的最好的演算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的演算法來排序資料範圍很大的陣列。

演算法的步驟如下:

  1. 找出待排序的陣列中最大和最小的元素
  2. 統計陣列中每個值為i的元素出現的次數,存入陣列C的第i
  3. 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
  4. 反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1

貼上程式碼:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <time.h>
  4. //對於排序的關鍵字範圍,一定是0-99  
  5. #define NUM_RANGE (100)  
  6. void print_arr(int *arr, int n)  
  7. {  
  8.        int i;  
  9.        for(i=0; i
    <n; i++){  
  10.                if(!i){  
  11.                        printf("%d", arr[i]);  
  12.                }else{  
  13.                        printf(" %d", arr[i]);  
  14.                }  
  15.        }  
  16.        printf("\n");  
  17. }  
  18. /*  
  19. 演算法的步驟如下:  
  20.     1.找出待排序的陣列中最大和最小的元素  
  21.     2.統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項  
  22.     3.對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)  
  23.     4.反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1  
  24. */  
  25. void counting_sort(int *ini_arr, int *sorted_arr, int n)  
  26. {  
  27.        int *count_arr = (int *)malloc(sizeof(int) * NUM_RANGE);  
  28.        int i, j, k;  
  29.        //統計陣列中,每個元素出現的次數  
  30.        for(k=0; k<NUM_RANGE; k++){  
  31.                count_arr[k] = 0;  
  32.        }  
  33.        for(i=0; i<n; i++){  
  34.                count_arr[ini_arr[i]]++;  
  35.        }  
  36.        for(k=1; k<NUM_RANGE; k++){  
  37.                count_arr[k] += count_arr[k-1];  
  38.        }  
  39.        for(j=n-1 ; j>=0; j--){  
  40.            int elem = ini_arr[j];  
  41.            int index = count_arr[elem]-1;  
  42.            sorted_arr[index] = elem;  
  43.            count_arr[elem]--;  
  44.        }  
  45.        free(count_arr);  
  46. }  
  47. int main(int argc, char* argv[])  
  48. {  
  49.        int n;  
  50.        if(argc <2){  
  51.                n = 10;  
  52.        }else{  
  53.                n = atoi(argv[1]);  
  54.        }  
  55.        int i;  
  56.        int *arr = (int *)malloc(sizeof(int) * n);  
  57.        int *sorted_arr = (int *)malloc(sizeof(int) *n);  
  58.        srand(time(0));  
  59.        for(i=0; i<n; i++){  
  60.                arr[i] = rand() % NUM_RANGE;  
  61.        }  
  62.        printf("ini_array: ");  
  63.        print_arr(arr, n);  
  64.        counting_sort(arr, sorted_arr, n);  
  65.        printf("sorted_array: ");  
  66.        print_arr(sorted_arr, n);  
  67.        free(arr);  
  68.        free(sorted_arr);  
  69.        return 0;  
  70. }  

桶排序的基本思想

假設有一組長度為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個左右。

但是,對比桶排序,基數排序每次需要的桶的數量並不多。而且基數排序幾乎不需要任何“比較”操作,而桶排序在桶相對較少的情況下,桶內多個數據必須進行基於比較操作的排序。因此,在實際應用中,基數排序的應用範圍更加廣泛。