算法基础篇--数据排序(三):计数排序
不学不知道,算法真奇妙。又到了将“算法”刻到骨子里的时刻。这一讲我们开始算法基础篇之数据排序的第三讲——“计数排序”。
1、前言
第一讲中,数据排序算法的时间复杂度为O(n²)。到第二讲时,数据排序算法的时间复杂度已经优化到了O(nlogn)。既然还有第三讲,那应该意味着我们还能再继续优化。没错,本讲要介绍的“计数排序”就是时间复杂度为O(n+m)的排序算法,这里优化的时间是通过牺牲空间O(m)来换取的,同时,该算法还有一定的场景限制。
2、计数排序
不同于之前的排序算法都要进行元素的比较,计数排序是一种分配类的排序算法。简单来说就是一种简单的数据元素(非负整数)与数组下标的映射。如对数组a={9,8,5,6,6,3,6,2,1,1}进行计数排序。首先遍历得到数组的最大值9和最小值1,由此得到最大值和最小值的差值sub = 9-1 = 8,并据此生成至少容量为8的整型数组。我们新建数组b。接着,遍历原数组a,针对a[0]中对应的值9,在数组b中更新b[9]++,即原数组中的数据成了计数数组中的下标。以此类推,比如对于数组a中的a[3],a[4],a[6]中的数据都是“6”,遍历的时候则分别对b[6]进行“增1”操作,最终b[6]=3。上述分配的结果可以参看下图:
如何体现最终的排序效果呢?遍历数组b,根据数组中的数据确定输出对应元素的下标的次数。如对升序,我们从b[0]开始遍历,b[0]=0,对“0”输出0次;接下来是b[1],b[1]=2,输出两次下标“1”,此时生成的输出内容为:“1,1”。接着是b[2],b[2]=1,输出依次下标"2",此时生成的输出内容为:“1,1,2”。依次类推,最后输出的结果即为:“1,1,2,3,5,6,6,6,8,9”。也就是a数组的升序排列了。实际场景中,通常会根据原始数组中的最小值来做偏移,以达到节省空间的目的,而空间的大小通常利用memset开辟为(“最大值”-“最小值”+1)的的区间范围,详情请通过阅读以下代码加以理解:
using namespace std;
const int N = 10010,INF = 1e8;
int a[N],b[N],n;
int main(){
srand((unsigned)time(NULL));
n = rand()%10+10;
//这里生成的数据控制在10到30,方便对比
//为方便描述,这里数据从下标为0开始存储
int maxa = -INF , mina = INF;
printf("待排序数组:\n");
for(int i=0;i<n;++i){
a[i] = rand()%20+10;
printf("%d ",a[i]);
maxa = max(maxa,a[i]);//获取最大值
mina = min(mina,a[i]);//获取最小值
}
int range = maxa - mina;//计算极差
//开始计数排序
memset(b,0,range+1);//生成数组,注意生成的大小应该是range+1
for(int i=0;i<n;++i){
//注意这里是分配操作,为控制数组区间,这里需要减去最小值进行偏移。
b[a[i]-mina]++;
}
printf("\n\n计数数组结果(下标偏移量为%d):\n",mina);
for(int i=0;i<=range;++i){
printf("b[%d]=%d ",i,b[i]);
}
printf("\n\n计数排序结果:\n");
for(int i=0;i<=range;++i){
for(int j=1;j<=b[i];++j)//对应下标i输出的次数
printf("%d ",i+mina);//输出下标要加上偏移值
}
return 0;
}
执行效果如下:
3、算法思想应用
下面这道题是NOIP2006普及组真题,选至一本通第1184题《明明随机数》。
**************************************************
【题目描述】
明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了N个1到1000之间的随机整数(N≤100),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。
【输入】
有2行,第1行为1个正整数,表示所生成的随机数的个数:N;
第2行有N个用空格隔开的正整数,为所产生的随机数。
【输出】
也是2行,第1行为1个正整数M,表示不相同的随机数的个数。第2行为M个用空格隔开的正整数,为从小到大排好序的不相同的随机数。
【输入样例】
10
20 40 32 67 40 20 89 300 400 15
【输出样例】
8
15 20 32 40 67 89 300 400
**************************************************
分析:考虑N是1000以内的随机自然数,涉及的是非负整数类型且在较小范围内的排序问题,因此考虑用计数数组来快速实现。建立计数数组a,对输入的元素依此映射到计数数组对应的下标中,因为涉及到“去重”并需统计随机数的个数,因此这里利用计数数组中的值是否不为0来判断是否对该数组元素进行计数并统计随机数个数。参考代码如下:
using namespace std;
const int N = 1005;
int a[N],n,t,res;
int main() {
scanf("%d",&n);
for(int i=1; i<=n; ++i) {
scanf("%d",&t);
if(!a[t])
a[t] = 1 , res++;//更新计数数组并累计随机数
}
printf("%d\n",res);//输出随机数个数
for(int i=1; i<=1000; ++i) {
if(a[i]) printf("%d ",i);//升序输出
}
return 0;
}
4、小结
计数排序的算法思想总让人联想到“表驱动法”,这是我在程序员圣经《代码大全》里学到的第一个非常非常有用的方法,在之前的工作中屡有应用,有兴趣的同学也可以提前了解下。今天元旦,新的flag立起来。还有,面对烦恼和茫然,大声说:管它呢,我总要活下去。这一讲就先到这里,谢谢大家的时间。