漫谈经典排序算法:五、线性时间排序(计数、基数、桶排序)

1、序言

这是《漫谈经典排序算法系列》第五篇,给出了三种线性时间排序,分别是计数排序、基数排序、桶排序

各种排序算法的解析请参考如下:

 

《漫谈经典排序算法:一、从简单选择排序到堆排序的深度解析》

《漫谈经典排序算法:二、各种插入排序解析及性能比较》

《漫谈经典排序算法:三、冒泡排序 && 快速排序》

《漫谈经典排序算法:四、归并排序》

《漫谈经典排序算法:五、线性时间排序(计数、基数、桶排序)》

《漫谈经典排序算法:六、各种排序算法总结》

注:为了叙述方便,本文以及源代码中均不考虑A[0],默认下标从1开始。

2、计数排序

          2.1 引出

            前面四篇博客中,所有的排序算法都存在比较,都可以称为”比较排序“。比较排序的下界为o(nlogn)。那么有没有时间复杂度为o(n)的线性时间排序算法呢?计数排序便是很基础的一种线性时间排序,它是基数排序的基础。基本思想是:对每一个元素x,确定小于x的元素个数,就可以把x直接放到它在有序序列中的位置上。过程描述:假设待排序序列a中值的范围[0,k],其中k表示待排序序列中的最大值。首先用一个辅助数组count记录各个值在a中出现的次数,比如count[i]表示i在a中的个数。然后依次改变count中元素值,使count[i]表示a中不大于i的元素个数。然后从后往前扫描a数组,a中的元素根据count中的信息直接放到辅助数组b中。最后把有序序列b复制到a。

          2.2 代码

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3.   
  4. //计数排序,n为数组a的记录个数,k为记录中最大值  
  5. void countingSort(int *a,int n,int k)  
  6. {  
  7.     int i;  
  8.     int *count=(int *)malloc(sizeof(int)*(k+1));  
  9.     int *b=(int *)malloc(sizeof(int)*(n+1));  
  10.     //初始化计数数组count  
  11.     for(i=0;i<=k;i++)  
  12.         *(count+i)=0;  
  13.     //计算等于a[i]的记录个数  
  14.     for(i=1;i<=n;i++)  
  15.         (*(count+a[i]))++;  
  16.     //计算小于等于a[i]的记录个数  
  17.     for(i=1;i<=k;i++)  
  18.         *(count+i) += *(count+i-1);  
  19.     //扫描a数组,把各个元素放在有序序列中相应的位置上  
  20.     for(i=n;i>=1;i--){  
  21.         *(b + *(count + a[i]))=a[i];  
  22.         (*(count+a[i]))--;   
  23.     }  
  24.     for(i=1;i<=n;i++)  
  25.         a[i]=*(b+i);  
  26.     free(count);  
  27.     free(b);  
  28. }  
  29.   
  30. void main()  
  31. {  
  32.     int i;  
  33.     int a[7]={0,3,5,8,9,1,2};//不考虑a[0]  
  34.     countingSort(a,6,9);  
  35.     for(i=1;i<=6;i++)  
  36.         printf("%-4d",a[i]);  
  37.     printf("\n");  
  38. }  

          2.3 效率分析

从代码来看,计数排序有5个for循环,其中三个时间是n,两个时间是k。所以总时间T(3n+2k),时间复杂度o(n+k),不管是在最坏还是最佳情况下,此时间复杂度不变.此外,计数排序是稳定的,辅助空间n+k,这个空间是比较大的,计数排序对待排序序列有约束条件(如前面我们假设待排序序列a中值的范围[0,k],其中k表示待排序序列中的最大值),元素值需是非负数,k太大的话会大大降低效率。这里要注意的是 “扫描a数组把各个元素放在有序序列相应的位置上” 这步为什么要从后往前扫描a数组呢?大家想一想计数排序的过程就知道,因为从前扫描导致计数排序不稳定,前面说了,计数排序是基数排序的基础,所以它的稳定性直接影响到基数排序的稳定。

3、基数排序

          3.1 引出

            在计数排序中,当k很大时,时间和空间的开销都会增大(可以想一下对序列{8888,1234,9999}用计数排序,此时不但浪费很多空间,而且时间方面还不如比较排序)。于是可以把待排序记录分解成个位(第一位)、十位(第二位)....然后分别以第一位、第二位...对整个序列进行计数排序。这样的话分解出来的每一位不超过9,即用计数排序序列中最大值是9.

          3.2 代码

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<math.h>  
  4.   
  5.   
  6. //计数排序,n为数组a的记录个数,k为记录中最大值,按第d位排序  
  7. void countingSort(int *a,int n,int k,int d)  
  8. {  
  9.     int i;  
  10.     int *count=(int *)malloc(sizeof(int)*(k+1));  
  11.     int *b=(int *)malloc(sizeof(int)*(n+1));  
  12.     //初始化计数数组count  
  13.     for(i=0;i<=k;i++)  
  14.         *(count+i)=0;  
  15.     //计算等于a[i]在d位(a[i]/(int)pow(10,d-1)%10)的记录个数  
  16.     for(i=1;i<=n;i++)  
  17.         (*(count+a[i]/(int)pow(10,d-1)%10))++;  
  18.   
  19.     //计算小于等于a[i]在d位(a[i]/(int)pow(10,d-1)%10)的记录个数  
  20.     for(i=1;i<=k;i++)  
  21.         *(count+i) += *(count+i-1);  
  22.     //扫描a数组,把各个元素放在有序序列中相应的位置上  
  23.     for(i=n;i>=1;i--){  
  24.         *(b + *(count + a[i]/(int)pow(10,d-1)%10))=a[i];  
  25.         (*(count+a[i]/(int)pow(10,d-1)%10))--;   
  26.     }  
  27.     for(i=1;i<=n;i++)  
  28.         a[i]=*(b+i);  
  29.     free(count);  
  30.     free(b);  
  31. }  
  32.   
  33.   
  34. //基数排序,n为数组a的记录个数,每一个记录中有d位数字  
  35. void radixSort(int *a,int n,int d)  
  36. {  
  37.     int i;  
  38.     for(i=1;i<=d;i++){  
  39.         countingSort(a,6,9,i);  
  40.     }  
  41. }  
  42.   
  43. void main()  
  44. {  
  45.     int i;  
  46.     int a[7]={0,114,118,152,114,111,132};//不考虑a[0]  
  47.     radixSort(a,6,3);  
  48.     for(i=1;i<=6;i++)  
  49.         printf("%-4d",a[i]);  
  50.     printf("\n");  
  51. }  


          3.3 效率分析

基数排序时间T(n)=d*(2k+3n),其中d是记录值的位数,(2k+3n)是每一趟计数排序时间,上文分析过了,k不超过9,d的值一般也很小,k、d都可以看成是一个很小的常数,所以时间复杂度o(n)。最坏最佳情况并不改变时间复杂度。基数排序是稳定的。辅助空间同计数排序k+n.

4、桶排序

          4.1 引出

            同计数排序一样,桶排序也对待排序序列作了假设,桶排序假设序列由一个随机过程产生,该过程将元素均匀而独立地分布在区间[0,1)上。基本思想是:把区间[0,1)划分成n个相同大小的子区间,称为桶。将n个记录分布到各个桶中去。如果有多于一个记录分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来记得到有序序列。

          4.2 代码

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3.   
  4. //桶排序  
  5. void bucketSort(double* a,int n)  
  6. {  
  7.     //链表结点描述  
  8.     typedef struct Node{  
  9.         double key;  
  10.         struct Node * next;   
  11.     }Node;  
  12.     //辅助数组元素描述  
  13.     typedef struct{  
  14.          Node * next;  
  15.     }Head;  
  16.     int i,j;  
  17.     Head head[10]={NULL};  
  18.     Node * p;  
  19.     Node * q;  
  20.     Node * node;  
  21.     for(i=1;i<=n;i++){  
  22.         node=(Node*)malloc(sizeof(Node));  
  23.         node->key=a[i];  
  24.         node->next=NULL;  
  25.         p = q =head[(int)(a[i]*10)].next;  
  26.         if(p == NULL){  
  27.             head[(int)(a[i]*10)].next=node;  
  28.             continue;  
  29.         }  
  30.         while(p){  
  31.             if(node->key < p->key)  
  32.                 break;  
  33.             q=p;  
  34.             p=p->next;  
  35.         }  
  36.         if(p == NULL){  
  37.             q->next=node;  
  38.         }else{  
  39.             node->next=p;  
  40.             q->next=node;  
  41.         }  
  42.     }  
  43.     j=1;  
  44.     for(i=0;i<10;i++){  
  45.         p=head[i].next;  
  46.         while(p){  
  47.             a[j++]=p->key;  
  48.             p=p->next;  
  49.         }  
  50.     }  
  51. }  
  52.   
  53. void main()  
  54. {  
  55.     int i;  
  56.     double a[13]={0,0.13,0.25,0.18,0.29,0.81,0.52,0.52,0.83,0.52,0.69,0.13,0.16};//不考虑a[0]  
  57.     bucketSort(a,12);  
  58.     for(i=1;i<=12;i++)  
  59.         printf("%-6.2f",a[i]);  
  60.     printf("\n");  
  61. }  


          4.3 效率分析

当记录在桶中分布均匀时,即每个桶只有一个元素,此时时间复杂度o(n)。因此桶排序适合对很少重复的记录排序。辅助空间2n。桶排序是稳定的排序,实现比较复杂。

5、附录

      参考书籍:  《算法导论》


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值