基數排序與桶排序,計數排序【詳解】


桶排序簡單入門篇^-^

在我們生活的這個世界中到處都是被排序過的東東。站隊的時候會按照身高排序,考試的名次需要按照分數排序,網上購物的時候會按照價格排序,電子郵箱中的郵件按照時間排序……總之很多東東都需要排序,可以說排序是無處不在。現在我們舉個具體的例子來介紹一下排序算法。

首先出場的是我們的主人公小哼,上面這個可愛的娃就是啦。期末考試完了老師要將同學們的分數按照從高到低排序。小哼的班上只有5個同學,這5個同學分別考了5分、3分、5分、2分和8分,哎,考得真是慘不忍睹(滿分是10分)。接下來將分數進行從大到小排序,排序后是8 5 5 3 2。你有沒有什么好方法編寫一段程序,讓計算機隨機讀入5個數然后將這5個數從大到小輸出?請先想一想,至少想15分鍾再往下看吧(*^__^*)。

我們這里只需借助一個一維數組就可以解決這個問題。請確定你真的仔細想過再往下看哦。

首先我們需要申請一個大小為11的數組int a[11]。OK,現在你已經有了11個變量,編號從a[0]~a[10]。剛開始的時候,我們將a[0]~a[10]都初始化為0,表示這些分數還都沒有人得過。例如a[0]等於0就表示目前還沒有人得過0分,同理a[1]等於0就表示目前還沒有人得過1分……a[10]等於0就表示目前還沒有人得過10分。

下面開始處理每一個人的分數,第一個人的分數是5分,我們就將相對應的a[5]的值在原來的基礎增加1,即將a[5]的值從0改為1,表示5分出現過了一次。

第二個人的分數是3分,我們就把相對應的a[3]的值在原來的基礎上增加1,即將a[3]的值從0改為1,表示3分出現過了一次。

注意啦!第三個人的分數也是5分,所以a[5]的值需要在此基礎上再增加1,即將a[5]的值從1改為2,表示5分出現過了兩次。

按照剛才的方法處理第四個和第五個人的分數。最終結果就是下面這個圖啦。
 

你發現沒有,a[0]~a[10]中的數值其實就是0分到10分每個分數出現的次數。接下來,我們只需要將出現過的分數打印出來就可以了,出現幾次就打印幾次,具體如下。

a[0]為0,表示“0”沒有出現過,不打印。

a[1]為0,表示“1”沒有出現過,不打印。

a[2]為1,表示“2”出現過1次,打印2。

a[3]為1,表示“3”出現過1次,打印3。

a[4]為0,表示“4”沒有出現過,不打印。

a[5]為2,表示“5”出現過2次,打印5 5。

a[6]為0,表示“6”沒有出現過,不打印。

a[7]為0,表示“7”沒有出現過,不打印。

a[8]為1,表示“8”出現過1次,打印8。

a[9]為0,表示“9”沒有出現過,不打印。

a[10]為0,表示“10”沒有出現過,不打印。

最終屏幕輸出“2 3 5 5 8”,完整的代碼如下。

 1     #include <stdio.h> 
 2     int main()  
 3     {  
 4         int a[11],i,j,t;  
 5         for(i=0;i<=10;i++)  
 6             a[i]=0;  //初始化為0  
 7           
 8         for(i=1;i<=5;i++)  //循環讀入5個數  
 9         {  
10             scanf("%d",&t);  //把每一個數讀到變量t中  
11             a[t]++;  //進行計數  
12         }  
13      
14         for(i=0;i<=10;i++)  //依次判斷a[0]~a[10]  
15             for(j=1;j<=a[i];j++)  //出現了幾次就打印幾次  
16                 printf("%d ",i);  
17      
18         getchar();getchar();   
19         //這里的getchar();用來暫停程序,以便查看程序輸出的內容  
20         //也可以用system("pause");等來代替  
21         return 0;  
22     } 

輸入數據為:

5 3 5 2 8 

仔細觀察的同學會發現,剛才實現的是從小到大排序。但是我們要求是從大到小排序,這該怎么辦呢?還是先自己想一想再往下看哦。

其實很簡單。只需要將for(i=0;i<=10;i++)改為for(i=10;i>=0;i--)就OK啦,快去試一試吧。

這種排序方法我們暫且叫它“桶排序”。因為其實真正的桶排序要比這個復雜一些,以后再詳細討論,目前此算法已經能夠滿足我們的需求了。

這個算法就好比有11個桶,編號從0~10。每出現一個數,就在對應編號的桶中放一個小旗子,最后只要數數每個桶中有幾個小旗子就OK了。例如2號桶中有1個小旗子,表示2出現了一次;3號桶中有1個小旗子,表示3出現了一次;5號桶中有2個小旗子,表示5出現了兩次;8號桶中有1個小旗子,表示8出現了一次。

現在你可以嘗試一下輸入n個0~1000之間的整數,將它們從大到小排序。提醒一下,如果需要對數據范圍在0~1000的整數進行排序,我們需要1001個桶,來表示0~1000之間每一個數出現的次數,這一點一定要注意。另外,此處的每一個桶的作用其實就是“標記”每個數出現的次數,因此我喜歡將之前的數組a換個更貼切的名字book(book這個單詞有記錄、標記的意思),代碼實現如下。

 1     #include <stdio.h> 
 2      
 3     int main()  
 4     {  
 5         int book[1001],i,j,t,n;  
 6         for(i=0;i<=1000;i++)  
 7             book[i]=0;   
 8         scanf("%d",&n);//輸入一個數n,表示接下來有n個數  
 9         for(i=1;i<=n;i++)//循環讀入n個數,並進行桶排序  
10         {  
11             scanf("%d",&t);  //把每一個數讀到變量t中  
12             book[t]++;  //進行計數,對編號為t的桶放一個小旗子  
13         }  
14         for(i=1000;i>=0;i--)  //依次判斷編號1000~0的桶  
15             for(j=1;j<=book[i];j++)  //出現了幾次就將桶的編號打印幾次  
16                  printf("%d ",i);  
17      
18         getchar();getchar();  
19         return 0;  
20     } 

可以輸入以下數據進行驗證。

10  

8 100 50 22 15 6 1 1000 999 0 

運行結果是:

1000 999 100 50 22 15 8 6 1 0 

最后來說下時間復雜度的問題。代碼中第6行的循環一共循環了m次(m為桶的個數),第9行的代碼循環了n次(n為待排序數的個數),第14行和第15行一共循環了m+n次。所以整個排序算法一共執行了m+n+m+n次。我們用大寫字母O來表示時間復雜度,因此該算法的時間復雜度是O(m+n+m+n)即O(2*(m+n))。我們在說時間復雜度的時候可以忽略較小的常數,最終桶排序的時間復雜度為O(m+n)。還有一點,在表示時間復雜度的時候,n和m通常用大寫字母即O(M+N)。

這是一個非常快的排序算法。桶排序從1956年就開始被使用,該算法的基本思想是由E.J. Issac和R.C. Singleton提出來的。之前我說過,其實這並不是真正的桶排序算法,真正的桶排序算法要比這個更加復雜!

下面具體來說說基數排序和桶排序吧!

基數排序

基本思想

不進行關鍵字的比較,而是利用”分配”和”收集”。

PS:以十進制為例,基數指的是數的位,如個位,十位百位等。而以十六進制為例,0xB2,就有兩個radices(radix的復數)。

Least significant digit(LSD)

短的關鍵字被認為是小的,排在前面,然后相同長度的關鍵字再按照詞典順序或者數字大小等進行排序。比如1,2,3,4,5,6,7,8,9,10,11或者”b, c, d, e, f, g, h, i, j, ba” 。

Most significance digit(MSD)

直接按照字典的順序進行排序,對於字符串、單詞或者是長度固定的整數排序比較合適。比如:1, 10, 2, 3, 4, 5, 6, 7, 8, 9和 “b, ba, c, d, e, f, g, h, i, j”。

基數排序圖示

從圖示中可以看出基數排序(LSD)的基本流程為如下節。

基數排序流程

將根據整數的最右邊數字將其扔進相應的0~9號的籃子里,對於相同的數字要保持其原來的相對順序(確保排序算法的穩定性),然后將籃子里的數如圖所示的串起來,然后再進行第二趟的收集(按照第二位的數字進行收集),就這樣不斷的反復,當沒有更多的位時,串起來的數字就是排好序的數字。

算法分析

空間

采用順序分配,顯然不合適。由於每個口袋都有可能存放所有的待排序的整數。所以,額外空間的需求為10n,太大了。采用鏈接分配是合理的。額外空間的需求為n,通常再增加指向每個口袋的首尾指針就可以了。在一般情況下,設每個鍵字的取值范圍為d,首尾指針共計2*radix,總的空間為O(n+2*radix)。

時間

上圖示中每個數計有2 位,因此執行2 次分配和收集就可以了。在一般情況下,每個結點有d 位關鍵字,必須執行d 次分配和收集操作。

  • 每次分配的代價:O(n)O(n)
  • 每次收集的代價:O(radix)O(radix)
  • 總的代價為:O(d*(n+radix))O(d×(n+radix))

算法的c++ plus plus實現

基於LSD的基數排序算法:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 const int MAX = 10;
 5 
 6 void print(int *a,int sz) {               
 7     for(int i = 0; i < sz; i++)
 8         cout << a[i] << " ";
 9     cout << endl;
10 }
11 
12 void RadixSortLSD(int *a, int arraySize)
13 {
14     int i, bucket[MAX], maxVal = 0, digitPosition =1 ;
15     for(i = 0; i < arraySize; i++) {
16         if(a[i] > maxVal) maxVal = a[i];
17     }
18 
19     int pass = 1;  // used to show the progress
20     /* maxVal: this variable decide the while-loop count 
21                if maxVal is 3 digits, then we loop through 3 times */
22     while(maxVal/digitPosition > 0) {
23         /* reset counter */
24         int digitCount[10] = {0};
25 
26         /* count pos-th digits (keys) */
27         for(i = 0; i < arraySize; i++)
28             digitCount[a[i]/digitPosition%10]++;
29 
30         /* accumulated count */
31         for(i = 1; i < 10; i++)
32             digitCount[i] += digitCount[i-1];
33 
34         /* To keep the order, start from back side */
35         for(i = arraySize - 1; i >= 0; i--)
36             bucket[--digitCount[a[i]/digitPosition%10]] = a[i];
37 
38         /* rearrange the original array using elements in the bucket */
39         for(i = 0; i < arraySize; i++)
40             a[i] = bucket[i];
41 
42         /* at this point, a array is sorted by digitPosition-th digit */
43         cout << "pass #" << pass++ << ": ";
44         print(a,arraySize);
45 
46         /* move up the digit position */
47         digitPosition *= 10;
48     }   
49  }
50 
51 int main()
52 {
53     int a[] = {170, 45, 75, 90, 2, 24, 802, 66};
54     const size_t sz = sizeof(a)/sizeof(a[0]);
55 
56     cout << "pass #0: ";
57     print(a,sz);
58     RadixSortLSD(&a[0],sz);
59     return 0;
60 }

輸出為:

1 pass #0: 170 45 75 90 2 24 802 66 
2 pass #1: 170 90 2 802 24 45 75 66 
3 pass #2: 2 802 24 45 66 170 75 90 
4 pass #3: 2 24 45 66 75 90 170 802 

首先統計10個籃子(或口袋)中各有多少個數字,然后從0~9數字的頻次分布(而不是頻次密度,有一個累加的過程),以確定“收集”整數時的位置下標所在。同時為了保證排序算法穩定,相同的數字保持原來相對位置不變,對原始數據表倒序遍歷,逐個構成收集后的數據表。例如,上圖所示,對於數字66,其所對應的頻次分布為8,也就是應當排在第8位,在數組中下標應該為7。而如果對於數字2和802,對應的頻次分布為4,那么對於數據表從后往前遍歷的話,對應802的下標為3,而2的下標2,這樣實際上就保證了排序算法的穩定性。

桶排序(bucket sort)

基本思想

桶排序的基本思想是將一個數據表分割成許多buckets,然后每個bucket各自排序,或用不同的排序算法,或者遞歸的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。它是一個分布式的排序,介於MSD基數排序和LSD基數排序之間。

基本流程

建立一堆buckets;
遍歷原始數組,並將數據放入到各自的buckets當中;
對非空的buckets進行排序;
按照順序遍歷這些buckets並放回到原始數組中即可構成排序后的數組。

圖示

算法的c++ plus plus描述

  1 #include <iostream>
  2 #include <iomanip>
  3 using namespace std;
  4 
  5 #define NARRAY 8  /* array size */
  6 #define NBUCKET 5 /* bucket size */
  7 #define INTERVAL 10 /* bucket range */
  8 
  9 struct Node 
 10 { 
 11     int data;  
 12     struct Node *next; 
 13 };
 14 
 15 void BucketSort(int arr[]);
 16 struct Node *InsertionSort(struct Node *list);
 17 void print(int arr[]);
 18 void printBuckets(struct Node *list);
 19 int getBucketIndex(int value);
 20 
 21 void BucketSort(int arr[])
 22 {   
 23     int i,j;
 24     struct Node **buckets;  
 25 
 26     /* allocate memory for array of pointers to the buckets */
 27     buckets = (struct Node **)malloc(sizeof(struct Node*) * NBUCKET); 
 28 
 29     /* initialize pointers to the buckets */
 30     for(i = 0; i < NBUCKET;++i) {  
 31         buckets[i] = NULL;
 32     }
 33 
 34     /* put items into the buckets */
 35     for(i = 0; i < NARRAY; ++i) {   
 36         struct Node *current;
 37         int pos = getBucketIndex(arr[i]);
 38         current = (struct Node *) malloc(sizeof(struct Node));
 39         current->data = arr[i]; 
 40         current->next = buckets[pos];  
 41         buckets[pos] = current;
 42     }
 43 
 44     /* check what's in each bucket */
 45     for(i = 0; i < NBUCKET; i++) {
 46         cout << "Bucket[" << i << "] : ";
 47             printBuckets(buckets[i]);
 48         cout << endl;
 49     }
 50 
 51     /* sorting bucket using Insertion Sort */
 52     for(i = 0; i < NBUCKET; ++i) {  
 53         buckets[i] = InsertionSort(buckets[i]); 
 54     }
 55 
 56     /* check what's in each bucket */
 57     cout << "-------------" << endl;
 58     cout << "Bucktets after sorted" << endl;
 59     for(i = 0; i < NBUCKET; i++) {
 60         cout << "Bucket[" << i << "] : ";
 61             printBuckets(buckets[i]);
 62         cout << endl;
 63     }
 64 
 65     /* put items back to original array */
 66     for(j =0, i = 0; i < NBUCKET; ++i) {    
 67         struct Node *node;
 68         node = buckets[i];
 69         while(node) {
 70             arr[j++] = node->data;
 71             node = node->next;
 72         }
 73     }
 74 
 75     /* free memory */
 76     for(i = 0; i < NBUCKET;++i) {   
 77         struct Node *node;
 78         node = buckets[i];
 79         while(node) {
 80             struct Node *tmp;
 81             tmp = node; 
 82             node = node->next; 
 83             free(tmp);
 84         }
 85     }
 86     free(buckets); 
 87     return;
 88 }
 89 
 90 /* Insertion Sort */
 91 struct Node *InsertionSort(struct Node *list)
 92 {   
 93     struct Node *k,*nodeList;
 94     /* need at least two items to sort */
 95     if(list == 0 || list->next == 0) { 
 96         return list; 
 97     }
 98 
 99     nodeList = list; 
100     k = list->next; 
101     nodeList->next = 0; /* 1st node is new list */
102     while(k != 0) { 
103         struct Node *ptr;
104         /* check if insert before first */
105         if(nodeList->data > k->data)  { 
106             struct Node *tmp;
107             tmp = k;  
108             k = k->next; 
109             tmp->next = nodeList;
110             nodeList = tmp; 
111             continue;
112         }
113 
114         for(ptr = nodeList; ptr->next != 0; ptr = ptr->next) {
115             if(ptr->next->data > k->data) break;
116         }
117 
118         if(ptr->next!=0){  
119             struct Node *tmp;
120             tmp = k;  
121             k = k->next; 
122             tmp->next = ptr->next;
123             ptr->next = tmp; 
124             continue;
125         }
126         else{
127             ptr->next = k;  
128             k = k->next;  
129             ptr->next->next = 0; 
130             continue;
131         }
132     }
133     return nodeList;
134 }
135 
136 int getBucketIndex(int value)
137 {
138     return value/INTERVAL;
139 }
140 
141 void print(int ar[])
142 {   
143     int i;
144     for(i = 0; i < NARRAY; ++i) { 
145         cout << setw(3) << ar[i]; 
146     }
147     cout << endl;
148 }
149 
150 void printBuckets(struct Node *list)
151 {
152     struct Node *cur = list;
153     while(cur) {
154         cout << setw(3) << cur->data;
155         cur = cur->next;
156     }
157 }
158 
159 int main(void)
160 {   
161     int array[NARRAY] = {29,25,3,49,9,37,21,43};
162 
163     cout << "Initial array" << endl;
164     print(array);
165     cout << "-------------" << endl;
166 
167     BucketSort(array); 
168     cout << "-------------" << endl;
169     cout << "Sorted array"  << endl;
170     print(array); 
171     return 0;
172 }

輸出為:

 1 Initial array
 2  29 25  3 49  9 37 21 43
 3 -------------
 4 Bucket[0] :   9  3
 5 Bucket[1] :
 6 Bucket[2] :  21 25 29
 7 Bucket[3] :  37
 8 Bucket[4] :  43 49
 9 -------------
10 Bucktets after sorted
11 Bucket[0] :   3  9
12 Bucket[1] :
13 Bucket[2] :  21 25 29
14 Bucket[3] :  37
15 Bucket[4] :  43 49
16 -------------
17 Sorted array
18   3  9 21 25 29 37 43 49

雖然程序中的bucket采用鏈表結構以充分利用空間資源,而且bucket的構造也很巧妙,做的數據結構類似於棧鏈表的形式,插入都是插到頂部,所以后遍歷的數據總是會在上面,因此從放入bucket之后的輸出可以看出,跟圖示進行對比,發現確實跟原來的原始相對順序相反。

計數排序(counting sort)

目前介紹的利用比較元素進行排序的方法對數據表長度為n的數據表進行排序時間復雜度不可能低於O(nlogn)。但是如果知道了一些數據表的信息,那么就可以實現更為獨特的排序方式,甚至是可以達到線性時間的排序。

基本思想

當數據表長度為n,已知數據表中數據的范圍有限,比如在范圍0k之間,而k又比n小許多,這樣可以通過統計每一個范圍點上的數據頻次來實現計數排序。

基本操作

根據獲得的數據表的范圍,分割成不同的buckets,然后直接統計數據在buckets上的頻次,然后順序遍歷buckets就可以得到已經排好序的數據表。

算法的c++ plus plus實現

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void print(int a[], int sz) {
 6     for (int i = 0; i < sz;  i++ ) cout << a[i] << " ";
 7     cout << endl;
 8 }
 9 
10 void CountingSort(int arr[], int sz) {
11     int i, j, k;
12     int idx = 0;
13     int min, max;
14 
15     min = max = arr[0];
16     for(i = 1; i < sz; i++) {
17         min = (arr[i] < min) ? arr[i] : min;
18         max = (arr[i] > max) ? arr[i] : max;
19     }
20 
21     k = max - min + 1;
22     /* creates k buckets */
23     int *B = new int [k]; 
24     for(i = 0; i < k; i++) B[i] = 0;
25 
26     for(i = 0; i < sz; i++) B[arr[i] - min]++;
27     for(i = min; i <= max; i++) 
28         for(j = 0; j < B[i - min]; j++) arr[idx++] = i;
29 
30     print(arr,sz);
31 
32     delete [] B;
33 }
34 
35 int main()
36 {
37     int a[] = {5,9,3,9,10,9,2,4,13,10};
38     const size_t sz = sizeof(a)/sizeof(a[0]);
39     print(a,sz);
40     cout << "----------------------\n" ;
41     CountingSort(a, sz);
42 }

輸出為:

1 5 9 3 9 10 9 2 4 13 10
2 ----------------------
3 2 3 4 5 9 9 9 10 10 13

計算排序構造了k個buckets來統計數據頻次,共需要兩趟來實現排序,第一趟增量計數進行統計,第二趟將計數統計的對應的數重寫入原始數據表中。

因為這種排序沒有采用比較,所以突破了時間復雜度O(nlogn)的上線,但是counting sort又不像是一種排序,因為在復雜數據結構中,它不能實現同結構體中排序碼的排序來對結構體進行排序。也就不要提穩定與否了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM