博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线性时间排序:计数排序、基数排序、桶排序
阅读量:3735 次
发布时间:2019-05-22

本文共 5435 字,大约阅读时间需要 18 分钟。

对于内排序算法,从最初的时间复杂度O(n*n)改进到O(nlogn)。常见的归并排序和堆排序达到了最坏情况的上界,快速排序在平均情况下达到上界。(时间复杂度为O(nlogn))。这些算法都有一个有趣的性质:在排序的最终结果中,各元素的次序依赖于它们之间的比较,因此这类算法被称为比较排序。

对于排序算法我们可知,在最坏情况下,任何比较排序算法都需要做O(nlgn)次比较。在算法导论第八章中的证明中给出了详细的解释,这里只简单的解释其大致原理:对于比较排序算法而言,可以抽象其为完全二叉树的决策树。要求对应算法的比较次数,对任意结点的比较次数即为从根结点到该结点的路径长度,而根结点到该结点的长度即为该二叉树的高度。对于n个结点的完全二叉树而言,其高度为[log2n]+1,对n个结点进行比较,即得其比较次数为O(nlgn)。 因此,归并排序和堆排序是渐近最优的,并且任意已知的比较排序最多在常数因子上优于它们。

本次将要介绍的三种线性时间复杂度的算法:计数排序、基数排序和桶排序,是用运算而不是比较来确定排序顺序的。因此下界O(nlgn)对它们是不适用的。

计数排序(Count Sort)

计数排序的基本思想就是对每一个输入元素x,确定小于x的元素的个数,这样就可以把x直接放在它在最终输出数组的位置上。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)。

从算法导论提供的思路可知实现步骤如下:

  • 确定待排序的数组中最大的和最小的元素(检测是否适合使用计数排序)
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
/*A[0..len-1]为待排序数组,其中每个元素的值i满足0 <= i <= k。B[0..len-1]为排序后输出数组。C[0..k]提供临时存储空间,存储每个元素出现的次数。*/void CountSort(int A[],int B[],int k,int len)//k为待排序数组最大值,len为待排序数组长度{    int* C = (int* )malloc((k+1) * sizeof(int));    int i;    for(i = 0;i < k + 1;++i)//清空数组。        C[i] = 0;    for(i = 0;i < len;++i) //统计待排数组中每个元素出现次数。        C[A[i]] += 1;    for(i = 1;i < k + 1;++i)  //确定对于每一个元素,有多少输入元素是小于或等于当前元素值。        C[i] = C[i] + C[i-1];    for(i = len - 1;i >= 0;--i)    {        B[C[A[i]] - 1] = A[i];//对于每一个A[j]值而言,C[A[j]]即为A[j]在输出数组中的最终正确位置。        C[A[i]] = C[A[i]] - 1; //确保当下一个值等于A[j]的输入元素时(如果存在),该元素能直接被放到输出数组中A[j]的前一个位置。    }    free(C);}

当我们刚刚统计出C,C[i]可以表示A中值为i的元素的个数,此时我们直接顺序地扫描C,就可以求出排序后的结果。的确是这样,不过这种方法不再是计数排序,而是桶排序,确切地说,是桶排序的一种特殊情况。

void CountSort(int *arr,int k,int len)  {      int* crr = (int* )malloc((k+1) * sizeof(int));    int i,j=0;       for(i = 0;i< k + 1;i++)          crr[i] = 0;       for(i = 0;i < len;i++)          crr[arr[i]]++;      for(i = 0;i <= k;i++)          while((crr[i]--)>0)          {              arr[j++] = i;  //按序扫描crr序列并将对应值写入输出数组        }      free(crr);}

当输入的元素是0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k)。同时,计数排序的一个重要性质就是它是稳定的:具有相同值的元素在输出数组中的相对次序与他们在输入数组中的相对次序相同。

基数排序(Radix Sort)

基数排序(Radix Sort)是一种非比较型排序算法,它将整数按位数切割成不同的数字,然后按每个位分别进行排序。基数排序的方式可以采用MSD(Most significant digital)或LSD(Least significant digital),MSD是从最高有效位开始排序,而LSD是从最低有效位开始排序。当然我们可以采用MSD方式排序,按最高有效位进行排序,将最高有效位相同的放到一堆,然后再按下一个有效位对每个堆中的数递归地排序,最后再将结果合并起来。但是,这样会产生很多中间堆(高位排序比低位排序多了空间开销)。所以,通常基数排序采用的是LSD方式。

LSD基数排序实现的基本思路是将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。需要注意的是,对每一个数位进行排序的算法必须是稳定的,否则就会取消前一次排序的结果。通常我们使用计数排序或者桶排序作为基数排序的辅助算法。

/* 在第一种计数排序的实现形式上做了些修改 计数排序后的顺序为从小到大 arr[0...len-1]为待排数组,我们这里采用三位数 brr[0...len-1]为排序后的有序数组 w[0...len-1]用来保存取出的每一位上的数,其每个元素均是0-k中的一个值 crr[0...k]保存0...k中每个值出现的次数 */  void Count_Sort(int *arr,int *brr,int *w,int *crr,int len,int k)  //这里的代码实现借鉴于其他博客,逻辑很好理解因此直接贴在这里{      int i;      //数组crr各元素置0      for(i=0;i<=k;i++)          crr[i] = 0;      //统计数组w中每个元素重复出现的个数      for(i=0;i
=0;i--) { brr[crr[w[i]]-1] = arr[i]; //如果有相同的元素,则放在下一个位置上 crr[w[i]]--; } //再将brr中的元素复制给arr,这样arr就有序了 for(i=0;i

基数排序的时间复杂度为O(d(n+k))(使用稳定算法排序的前提下),d为数的位数。当d为常数且k为O(n)时,基数排序具有线性的时间代价。基数排序的空间复杂度为O(k*n),其中k为数的位数。基数排序和比较排序算法的选择依赖于具体实现和底层硬件的特性(快速排序通常可以比基数排序更加有效地使用硬件的缓存),以及输入数据的特性。同时,利用计数排序作为中间稳定排序的基数排序不是原址排序,而大多数比较排序为原址排序。因此,当主存的容量较宝贵时,会更加倾向于选择原址排序。

桶排序(Bucket Sort)

桶排序假设输入数据服从均匀分布,平均情况下它的时间代价为O(n)。具体来说,桶排序假设输入是由一个随机过程产生的,该过程将元素均匀、独立地分布在[0,1)区间上。桶排序将[0,1)区间划分为n个大小相同的子区间,也称为桶。然后将n个输入数分别放到桶中,每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。最后遍历每个桶,按次序将数据输出。

typedef struct node{      int key;      struct node * next;  }KeyNode;  void inc_sort(int keys[],int size,int bucket_size){      KeyNode **bucket_table=(KeyNode **)malloc(bucket_size*sizeof(KeyNode *));      for(int i=0;i
key=0; //记录当前桶中的数据量 bucket_table[i]->next=NULL; } for(int j=0;j
key=keys[j]; node->next=NULL; //映射函数计算桶号 int index=keys[j]/10; //初始化P成为桶中数据链表的头指针 KeyNode *p=bucket_table[index]; //该桶中还没有数据 if(p->key==0){ bucket_table[index]->next=node; (bucket_table[index]->key)++; }else{ //链表结构的插入排序 while(p->next!=NULL&&p->next->key<=node->key) p=p->next; node->next=p->next; p->next=node; (bucket_table[index]->key)++; } } //打印结果 for(int b=0;b
next; k!=NULL; k=k->next) cout<
key<<" "; cout<

桶排序代价分析

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的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),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

基数排序过程动画演示:

桶排序过程动画演示:

这篇博客很大程度上是借鉴别人的博客,虽然大部分是自己整理的。为了尊重借鉴博主的劳动成果,因此发为转载。对文中可能出现的错误,希望大家指正。

借鉴书籍 :《算法导论》

借鉴博客链接

你可能感兴趣的文章
mapreducr-分组GroupingComparator
查看>>
mapreduce-自定义outputformat
查看>>
模型表示及代价函数
查看>>
mapredcue-Redcuejoin
查看>>
C语言输出菱形(C笔记)
查看>>
C语言向文件写入学生信息并读取显示出来
查看>>
C语言删除字符数组中指定的字符(C笔记)
查看>>
C语言判断回文字符串(C笔记)
查看>>
C语言打印杨辉三角(C笔记)
查看>>
C语言数组旋转问题(C笔记)
查看>>
Keras软件安装
查看>>
cuda安装
查看>>
Zmapv6源码安装
查看>>
Anaconda3换源配置
查看>>
操作中划线-开头的文件
查看>>
Unsafe.putOrderedXXX系列方法详解(数组赋值的第二种方式)
查看>>
Netty对象池
查看>>
Netty写数据(动画)
查看>>
JVM(一)
查看>>
Java之枚举、注解、反射
查看>>