zoukankan      html  css  js  c++  java
  • 基数排序与桶排序,计数排序【详解】

    桶排序简单入门篇^-^

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

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

  • 相关阅读:
    二分图最大匹配
    Problems about trees
    Hackerrank Going to the Office
    多校题解
    HDU #2966 In case of failure
    K-D Tree
    UOJ #10 pyx的难题
    bzoj 1090 字符串折叠
    uva 1347 旅行
    bzoj 1059 矩阵游戏
  • 原文地址:https://www.cnblogs.com/ECJTUACM-873284962/p/6935506.html
Copyright © 2011-2022 走看看