引言
只要设计到数据,就会涉及到数据的排序问题,比如给你随机给你十个数 3,2,2,5,4,0,5,4,5,1 让你进行排序,那我们该怎样才是实现对这些整数的排序呢 ?
答案是多种多样的,比如用冒泡排序、希尔排序、计数排序、归并排序、快速排序等等,这些排序方法都可以实现对整数排序,而这篇文章要讲的就是计数排序
本文将从以下几个问题对计数排序进行分析和讲解:
- 什么是简单的计数排序?它存在哪些问题?
- 怎样解决简单计数排序存在的问题?
- 计数排序的完整代码
- 计数排序的代码详解。
什么是计数排序?
下面看百度百科对计数排序的定义:
计数排序是一个非基于比较的排序,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)。
简单理解:既然排序名字是计数排序,那么肯定要有统计数据这个过程。
下面先看一个简单的例子,咱们先用一种统计数据的方法对 3,2,2,5,4,0,5,4,5,1 这十个数排序。
经过观察发现,上面的十个数中,0出现了一次,1出现了一次,2出现了两次,3出现了一次,4出现了两次,5出现三次。
我们就可以用一个数组来存这些次数(数组下标代表存数的值,数组的值代表存了几次),
即count[0]=1,count[1]=1,count[2]=2,count[3]=1,count[4]=2,count[5]=3
然后再按顺序把这些统计的数打印出来是不是就完成了一个简单的排序?,下面看代码(我相信你一定能看懂)
#include<iostream> using namespace std; int main() { //要排序的数组 int arr[]={3,2,2,5,4,0,5,4,5,1}; int len=10;//要排序的数组长度 int count[6]={0};//这count数组长度6(5+1)是根据要排序数的最大值+1确定的 for(int i=0;i<len;i++)//统计各个数出现的次数 count[arr[i]]++; for(int i=0;i<6;i++)//把统计的格数打印出来 { while(count[i]!=0) { cout<<i<<" "; count[i]--; } } return 0; }
运行结果:0 1 2 2 3 4 4 5 5 5(是不是就是实现了简单的计数排序)
上面的过程其实就是简单的计数排序,他的精髓就在他统计各个数的个数,不过他存在两个问题,下面先看一个图片。
我们现在需要对上面的学生分数进行排序(从小到大)。
如果按照上面简单的计数排序,那就要开辟count数组时,把他的长度设置成96(95+1).然后再统计各个分出的出现次数。
存在的两个问题:
- 一旦我们按照上面的方法操作,不知道你发现没有,这个count数组,下标为0到89的长度,都用不到(因为没有分数为0-89的人)。那这样是不是造成了空间的浪费。(空间浪费问题)
- 像B和C这两个人,都是92分,排序前B同学在C同学,那么排序后,还能保证B同学在C同学前面吗?(稳定性问题)
怎样解决简单计数排序存在的问题?
一、先说怎么解决上面的空间浪费问题。
上面简单计数排序的统计数组下标的对应的分数,我们就可以从这入手,因为我们要排序的都是在90-95分之间,那我们可不可以只存0-5,比如某个人他90分,就存到count[0]中,如果是93分,就存到count[3]中。
上面的就是解决办法, 可以让所有的人,在统计时,把所有人的分数都减去这些人分数的最小值。那这样就可以开辟一个小数组了,就不会浪费空间了,下面看实现这个方法。
我们要排序的的分数最大值是95分,最小值是90分,那这样我们是不是就可以开辟一个长度为6(95-90+1)的的数组
所以代码的顺序就是:
- 首先求出来数组的最大值和最小值。
- 开辟合适长度的数组(长度=最大值-最小值+1)
- 统计各个人的分数,统计时,每个人在count数组的位置就是他的分数减去数组的最低分。
count[arr[i]-minx]++;
这里就不放完整代码了,类比这简单的计数排序,自己可以动手写一下试试。
二、再说怎么解决稳定性问题(★★★难点★★★)
还是先看图。
(我不太清楚为啥通过下面的方法可以解决这个稳定性问题,但是下面的方法确实可以解决稳定性问题,我也不知道为啥,可能自己水平不够吧)下面就把这个方法说出来(一共分两步):
第一步:根据上面图片中的第二个表,我们根据 coun[i]=count[i]+count[i-1] 。我们可以推出来一个新表
现在我们看推导出来的count表,看一下推导出来count表的作用,
比如i=5时,count[i]=10。这就说明分数为95(最低分90+5)的人,排序后,他最后的位置应该是第十个,
比如i=0时,count[i]=1。这就说明分数为90(最低分90+0)的人,排序后,他最后的位置在第一个
上面的这个推导新表的过程用以for循环即可实现
//累加 for(int i=0;i<l;i++) { count[i]+=count[i-1]; }
第二步:首先开辟一个新数组temp,用来存储排序后的分数。再写一个for循环,逆向遍历成绩表(不是count表)。
比如我们遍历到 J 这个学生了,发现他的成绩是91分(91在count表中对应下标1),那他在count表中的下标就是1,再看count[1]=2。因此J这个学生就在数组的第二个位置,就可以把91分这个成绩存到temp数组的第二个位置(即下标为1,因为数组下标从0开始的)。然后再把count[1] 减1。如果再有91分的查到这个count表(当然这个表没有),那他的位置就是数组的第一个位置(count[1]=1)。
就比如我们遍历到 I 这个同学,发现他的分数为95分,那查count表发现,count[5]=10,因此I这个同学在数组的第十个位置,然后count[5]再减一,count[5]就变成了9,再次查到95分的同学(遍历到G同学同学时),再查count[5]=9,那么G同学就在数组的第九个位置,这样就解决了稳定性问题。实现了G同学在I同学的前面。注意遍历过程一定要是逆序。
计数排序的完整代码
下面看完整代码:
#include<iostream> using namespace std; //计数排序函数 稳定 void CountingSort(int arr[],int len) { int minx=99999,maxn=-99999; //下面for循环是求出来要排序数组的最大值和最小值 for(int i=0;i<len;i++) { if(arr[i]>maxn) maxn=arr[i]; if(arr[i]<=minx) minx=arr[i]; } //cout<<maxn<<" "<<minx<<endl; int l=maxn-minx+1;//变量 l 是要开辟数组的长度 int count[maxn-minx+1]={0}; //下面的代码是统计作用,不过统计时要减去数组最小值,方便存储 for(int i=0;i<len;i++) count[arr[i]-minx]++; //累加 for(int i=0;i<l;i++) { count[i]+=count[i-1]; } int temp[len]={0};//用来排序好的数组 for(int i=len-1;i>=0;i--)//逆序遍历。 { temp[count[arr[i]-minx]-1]=arr[i]; count[arr[i]-minx]--; } //经过上面的逆序遍历,现在temp数组就是排序好的成绩数组 //把排序好的数组放到arr数组中 ,方便后面的打印 for(int i=0;i<len;i++) arr[i]=temp[i]; } //输出数组的值 void printf(int arr[],int len) { for(int i=0;i<len;i++) cout<<arr[i]<<" "; cout<<endl; } int main() { //要排序的数组 int arr[]={93,92,92,95,94,90,95,94,95,91}; int len=10;//要排序的数组长度 //排序 CountingSort(arr,len); //输出 printf(arr,len); return 0; }
运行结果:
计数排序的代码详解。
- 上面的代码写了两个函数,第一个就是打印数组函数printf。
- 第二个就是计数排序的详细过程,我相信只要结合文章内容,代码你一定可以看懂
- 不过现在的计数排序还是有问题,比如要排序的值只有两个,分别是1和100000000.那么同计数排序仍然会浪费大量的空间(具体为啥自己可以思考下),这个算是计数排序的缺陷吧。
- 就上面的看来,我发现如果对高考学生的成绩进行排序,计数排序就是特别好的一个方法, 因为分数的区间不大,但是要排序的人特别多。
本文参考以及引用:
创作不易,如果本文对你起到了一些帮助,何不点个赞再走呢!!!