1、定义
计数排序是桶排序的一种特殊情况,当要排序的n个数据,所处的范围并不大的时候,比如最大值是K,我们就可以把数据划分为k个桶,每个桶内的数值都是相同的,省去了桶内排序的时间。
2、举例说明
高考查分系统:比如50万考生,满分900,最低0分,最高900分,那么可以对应901个桶,对应分数从0~900,根据考生的成绩划分到这901个桶里,桶内都是分数相同的考生,不需要排序。因此只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了50万个考生的成绩排序,所以时间复杂度是O(n)。
3、实例计数排序
假设只有八个考生,分数在0~5之间,考生分数为:A[8] = {2, 5, 3, 0, 2, 3, 0, 3},对这8个考生成绩排序。
分析:
考生的成绩从0~5,我们使用大小为6的数组C[6]表示桶,其中下标对应分数。不过C[6]存储的并不是考生,而是对应分数的考生个数,那么遍历一遍C[6],就可以得到考生个数。
从图中可以看出,分数为3的考生有3个,小于等于3的有4个,所以成绩为3的考生在排序之后的有序数组R[8]中,会在下标4、5、6三个位置。
如何快速计算每个考生在有序数组中的位置,思路如下:
我们对C[6]数组顺序求和,如下所示,C[k]里面就存储了小于等于k的考生个数:
我们从后往前依次扫描数组A,比如,扫描到3时,我们可以从数组C中取出下标为3的值7,意思就是分数小于等于3的考生一共有7人,也就是说3数数组R中的第7个元素(就是下标6对应的位置),当3放入R数组中,小于等于3的人数就剩6个,更新C[3]的值,减1等于6。
以此内推,当扫描到第二个3的时候,就放入R[5]中,当扫描完数组A后,数组R的数据就是有序的:
通过上面描述,代码如下:
// 计数排序
public static void countingSort(int[] arr, int n){
if(n <=1) return;
// 查找数组中的数据范围,找到最大值,当前默认最小值是0
int maxScore = arr[0];
for(int i=0; i<n ;i++){
if(maxScore < arr[i]){
maxScore = arr[i];
}
}
// 申请计数数组,下标大小 0 - max
int[] count = new int[maxScore + 1];
for (int i=0; i<=maxScore; i++){
count[i] = 0;
}
// 计算每个元素的个数,放入count中
for(int i=0; i<n; i++){
count[arr[i]]++;
}
// 依次累加
for(int i=1; i<=maxScore; i++){
count[i] = count[i] + count[i-1];
}
// 临时数组R,用于存储排序之后的结果
int[] r = new int[n];
// 计算排序 -- 这步就是对应到最后一幅图
for (int i=n-1; i>=0; i--){
int index = count[arr[i]];
r[index] = arr[i];
count[arr[i]]--;
}
// 拷贝结果到arr中
for (int i=0; i<n; i++){
arr[i] = r[i];
}
}
从代码中可以清晰的看出,主要分三步:1、申请范围数组;2、对每个元素进行计数,并且一次累加;3、遍历计数数组,将遍历结果存到临时数组R中。最后就得到有序数组。
注意:
计数排序只能用于用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了,而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型,那么在不改变其大小的情况下,转换为非负整数。
说明:以上整理自极客邦数据结构与算法之美课程。