前言
最后三种排序算法了,由于都不是基于比较的排序,因此这三种排序算法可以以线性时间运行。但是因为限制条件的特殊性,因此应用面没有基于元素比较的排序算法广,但是在很多特定的情况下还是蛮有用途的,而且效率极高。
计数排序
计数排序是建立在这样的前提条件下的:假设n个输入元素的每一个都是0到k区间内的一个整数,其中k为某个整数。因此我们后面所写的程序也只是针对0到k之间的元素进行排序,换句话说,排序元素中不能有负数。
计数排序的基本思想是:对一个输入元素x,先确定所有输入元素中小于x的元素个数,那么排序后x所在的位置也就明确了。比如,所有的输入元素中有10个元素小于x,那么排好序后x的位置序号就应该是11。当然,如果有相同元素,自然要放到相邻的位置上。
算法导论上给出了计数排序的很详细的伪代码,我们根据此伪代码,并设数组arr为输入数组,arr中的每个元素值在0到k之间,brr为排序后的输出数组,crr记录arr中每个元素出现的次数。写出代码如下:
/* 第一种形式实现计数排序 计数排序后的顺序为从小到大 arr[0...len-1]为待排数组,每个元素均是0-k中的一个值 brr[0...len-1]为排序后的输出数组 crr[0...k]保存0...k中每个值在数组arr中出现的次数 */ void Count_Sort(int *arr,int *brr,int *crr,int len,int k) { int i,j=0; //数组crr各元素置0 for(i=0;i<=k;i++) crr[i] = 0; //统计数组arr中每个元素重复出现的个数 for(i=0;i<len;i++) crr[arr[i]]++; //求数组arr中小于等于i的元素个数 for(i=1;i<=k;i++) crr[i] += crr[i-1]; //把arr中的元素放在brr中对应的位置上 for(i=len-1;i>=0;i--) { brr[crr[arr[i]]-1] = arr[i]; //如果有相同的元素,则放在下一个位置上 crr[arr[i]]--; } }
很明显上面代码的时间复杂度为O(n+k),但用到了brr来保存排序结果,我们可以它做些改进,使排序原地进行,如下:
/* 第二种形式实现计数排序 计数排序后的顺序为从小到大 arr[0...len-1]为待排数组,每个元素均是0-k中的一个值 crr[0...k]保存0...k中每个值在数组arr中出现的次数 */ void Count_Sort(int *arr,int *crr,int len,int k) { int i,j=0; //数组crr各元素置0 for(i=0;i<=k;i++) crr[i] = 0; //统计数组arr中每个元素重复出现的个数 for(i=0;i<len;i++) crr[arr[i]]++; //根据crr[i]的大小,将元素i放入arr适当的位置 for(i=0;i<=k;i++) while((crr[i]--)>0) { arr[j++] = i; } }
采用如下代码测试:
int main() { int i; //待排序数组,每个元素均在0-8之间 int arr[] = {2,1,3,8,6,0}; int brr[6]; int crr[9]; Count_Sort(arr,brr,crr,6,8); printf("计数排序后的结果为:"); for(i=0;i<6;i++) printf("%d ",brr[i]); printf("\n"); return 0; }
测试结果如下:
最后我们稍微总结下计数排序的特点:
1、不是基于比较的排序,因此可以达到线性排序时间;
2、采取空间换时间的思想,需要brr和crr等辅助空间,但是时间复杂度仅为O(n+k);
3、稳定性好,这也是计数排序最重要的一个特性。
在实际工作中,当k=O(n)时,我们一般才会采取计数排序,如果k很大,则不宜采取该算法,尤其在如下情形下:
待排序元素为:1、3、8、5、10000000,这样会造成很大的资源浪费。
基数排序
基数排序的排序时间也可以达到线性,尤其在k和d(后面介绍该参数)很小的情况下。
基数排序采取的是多关键字比较的策略,且每个关键字对排序的影响不同,根据关键字影响的主次,有两种排序方法:
1、先根据影响最大的关键字来排序,而后在该关键字相同的情况下,再根据影响次之的关键字来排序,依此类推,直到最后按照影响最小的关键字排序后,序列有序。我们称这个为先高位后低位。
2、先根据影响最小的关键字来排序,而后再对全部的元素根据影响次小的关键字来排序,依此类推,直到最后按照影响最大的关键字排序后,序列有序。我们称这个为先低位后高位。
这有点抽象,我们用具体的例子来说明。比如,我们希望用三个关键字(年、月、日)来对日期进行排序,按照基数排序的思想,则:
采取第一种方法排序的思路是这样的:先比较年,形成一个按照年有序排列的序列,而后对年相等的日期,在比较月,对月相等的日期,再比较日,最后得到有序序列。
采取第二种方法排序的思路是这样的:先对所有元素按照日排序,再对所有元素按照月排序,最后对所有元素按照年排序,得到有序序列。
我们一般采用第二种方法来进行排序,比如如下的数字序列:
217,125,362,136,733,522
先对个位上的数字进行排序,得到:
362,522,733,125,136,217
再对十位上的数字进行排序,得到:
217,522,125,733,136,362
最后对百位上的数字进行排序,得到:
125,136,217,362,522,733
很明显,高位数字比低位数字对排序的影响大,该方法的正确性很容易证明,这里不再说明。
我们注意到,这里每一步都需要对各个位上的数进行排序。而为了保证基数排序的正确性(稳定性),我们对每个位上的数进行排序时可以选用计数排序。算法导论上给的伪代码太粗略了,直接贴出自己写的完全代码(包括测试代码),如下:
/******************************* 基数排序 ********************************/ #include<stdio.h> #include<stdlib.h> /* 在第一种计数排序的实现形式上做了些修改 计数排序后的顺序为从小到大 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<len;i++) crr[w[i]]++; //求数组w中小于等于i的元素个数 for(i=1;i<=k;i++) crr[i] += crr[i-1]; //把arr中的元素放在brr中对应的位置上 for(i=len-1;i>=0;i--) { brr[crr[w[i]]-1] = arr[i]; //如果有相同的元素,则放在下一个位置上 crr[w[i]]--; } //再将brr中的元素复制给arr,这样arr就有序了 for(i=0;i<len;i++) { arr[i] = brr[i]; } } /* 基数排序后的顺序为从小到大 其中参数d为元素的位数 */ void Basic_Sort(int *arr,int *brr,int *w,int *crr,int len,int k,int d) { int i,j,val=1; //从低位到高位依次进行计数排序 for(i=1;i<=d;i++) { //w中保存的是arr中每个元素对应位上的数 //范围在0-k之间 for(j=0;j<len;j++) w[j] = (arr[j]/val)%10; //对当前位进行计数排序 Count_Sort(arr,brr,w,crr,len,k); val *= 10; } } int main() { int i; //待排序数组,每个元素的每一位均在0-7之间 int arr[] = {217,125,362,136,733,522}; int brr[6]; //用来保存每次计数排序后的结果 int w[6]; //每次循环时,保存该位上的数 int crr[8]; //每次循环时,保存该位上的数出现的次数 Basic_Sort(arr,brr,w,crr,6,7,3); printf("计数排序后的结果为:"); for(i=0;i<6;i++) printf("%d ",arr[i]); printf("\n"); return 0; }
测试结果如下:
最后我们同样对基数排序稍微做下总结:
1、同样不是基于比较的排序,因此可以达到线性排序时间;
2、同样采取空间换时间的思想,需要额外的辅助空间,但是时间复杂度仅为O(d(n+k));
3、基数排序的稳定性同样也很好。
吐槽一下:写Basic_Sort的时候,每一次的val应该是10的i-1次方,手误,打成了10*(i-1), 跟踪调试了一下午,每次循环w数组中的值都是个位数,就在那几行代码找问题,但尼玛我偏偏就是没看出来。
桶排序
桶排序假设输入数据服从均匀分布,平均情况下它的时间复杂度为O(n)。与计数排序类似,因为对输入数据作了某种假设,桶排序的速度也很快。
桶排序将输入元素按照一定的区间划分为若干个桶,因为假设了输入的数据在总的区间范围内是均匀分布的,因此一般不会出现很多个元素落在同一个桶中的情况。我们可以先对每个桶中的元素排序,而后按照桶的序号,依次把各个桶中的元素列出来即可。
在进行桶排序时,我们需要一个辅助数组来存放每个桶,而每个桶中的元素最好用链表串起来,这样操作起来比较方便。一个很普遍的展示图例如下:
桶排序了解思想即可,代码我们就不再实现了,因为它的实现不具备普遍性,要根据不同的情况来划分不同个数的桶,以及桶所规定的区间。
2014校招时创新工厂下的涂鸦移动有一道这样的面试题:
一个字符数组,里面的字符可能是a-z、A-Z、0-9.现在要求对数组进行排序,要求所有小写字符放在最前面,所有大写字符放在中间,所有数字放在最后。而且各部分内部分别有序。
这个很明显用桶排序来实现效果最佳,尤其在数据量非常大的情况下。
再比如,如下情况也是用桶排序的最佳时机(摘自百度百科)
海量数据
一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。
分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100=<score<=900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。
方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。
实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程