基数排序与桶排序,计数排序详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基数排序与桶排序,计数排序详解相关的知识,希望对你有一定的参考价值。

桶排序简单入门篇^-^

在我们生活的这个世界中到处都是被排序过的东东。站队的时候会按照身高排序,考试的名次需要按照分数排序,网上购物的时候会按照价格排序,电子邮箱中的邮件按照时间排序……总之很多东东都需要排序,可以说排序是无处不在。现在我们举个具体的例子来介绍一下排序算法。

技术分享

首先出场的是我们的主人公小哼,上面这个可爱的娃就是啦。期末考试完了老师要将同学们的分数按照从高到低排序。小哼的班上只有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(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又不像是一种排序,因为在复杂数据结构中,它不能实现同结构体中排序码的排序来对结构体进行排序。也就不要提稳定与否了。

以上是关于基数排序与桶排序,计数排序详解的主要内容,如果未能解决你的问题,请参考以下文章

算法计数排序桶排序和基数排序详解

九种经典排序算法详解(冒泡排序,插入排序,选择排序,快速排序,归并排序,堆排序,计数排序,桶排序,基数排序)

计数排序与桶排序python实现

经典排序算法和python详解:归并排序快速排序堆排序计数排序桶排序和基数排序

希尔排序与桶排序算法详解

基数排序