上一篇文章中我们讲到了一种时间复杂度为O(n)的排序算法–桶排序,今天我们来分析一下另一种时间复杂度为O(n)的排序算法–计数排序。
一、计数排序的原理
计数排序实际上是桶排序的一种延伸。在桶排序中,我们把要排序的n个数据均匀的分配到了m个桶中。而计数排序是当我们要排序的n个数据范围不大的时候,我们可以将n个数分配到n个桶中。这样每个桶中存储的都是相同的数据,省去了桶内的快速排序,从而节省了时间。
比如说我们每个人都经历过的高考,在查询成绩的时候我们可以看到自己的成绩以及所在省的排名。那么查分系统是怎么通过成绩快速的定位到我们所在的名次的呢?
我们尝试用计数排序算法来分析解决这个问题。假设我们高考满分为750分,加上0分这个数据,我们可以划分的桶的个数为751个,对应的分数为0-750分。根据所在省份所有考生的成绩,我们将每个考生的成绩对号入座,分别加入到对应分数的桶中。最后我们只需创建一个存储数据的结构,例如数组,按桶的编号由小到大分别将每个桶中的数据加入到数组中。那么,最后我们得到的数组就是一个按分数排好序的有序数组。
整个排序过程可以划分成两个部分,第一部分为入桶,将数据分别放入对应的桶中,这个过程我们只需要遍历一遍数据,所以时间复杂度为O(n),第二部分为整合,这个过程我们只需要按桶的序号,将桶中的数据依次放入同一个数组中即可,所以这个过程的时间复杂度也为O(n),将两部分的时间加起来,最后得到的时间复杂度还是为O(n)。
看完了上边的分析,我们可能会产生疑问,既然算法思想与桶排序思想一样,就是划分桶的个数不一样,那么为什么我们称这种算法为计数排序呢?这个问题将在第二部分得到解答。
二、计数排序的实现
我们来看这样一个例子,假设有10个考生,他们的分数区间在0分到5分之间,我们将这10个数据存储到数组A[10] 中。于是根据计数排序的原理,我们用数组来表示桶。0分-5分,也就是6个分数,我们分配一个容量为6的数组 C[6] 作为桶,其中数组的一个下标编号代表着一个桶,而C[6]数组中。如下图所示:
C[0]位置存放的数字为2,代表着在A[10]数组中,分数为0分的考生个数为2。以此类推,C[1]位置存放的数据为3,代表着在A[10]数组中,分数为1分的考生为3个
接下来我们还需要一个数组用来存放最终的结果,因为有10个学生的成绩,所以我们设这个数组为R[10] 。从上边的C[6]数组中我们可以看出,分数为1分的考生有3个,小于1分的考生有2个,所以成绩为1分的考生在R[10]数组中,会存放在下标为2、3、4的位置上。如下图所示:
那么,我们该如何快速计算出每个分数的考生在有序数组中对应的存储位置呢?
思路是这样的:我们对C[6]数组的定义做出一些改造,C[k]中存储的数据为小于等于分数k的考生的个数,于是C[6]数组就变成了下面这样:
准备工作做完之后,接下来的内容就是重点了!!!
举个例子,我们从前到后遍历数组A[10],比如说我们遍历到下标为3的元素,A[3]中存储的分数为1,我们想要找到1在R[10]数组中的位置。这时需要我们把关注点放在C[6]数组上,由于我们需要寻找的分数为1,那么只需查找C[1]位置上的数据,可以看到C[1] = 5。也就是说分数小于等于1的考生个数为5。即1是数组R[10]中的第5个元素(也就是数组R中下标为6的位置的元素)。当我们把1放入到R[10]数组中,小于等于1的考生个数就要减1,也就是C[1]位置的数字要减1,减完之后 C[1] = 4。
以此类推,当我们扫描到第二个分数为1的考生的时候,就会把它放入R[10]中的第6个元素的位置,也就是下标为5的位置。当我们对A[10]中的所有元素都进行了以上的操作后,R[10]中存放的就是排好序的有序数组了。
总结: 计数排序只能用在数据范围不大的场景中,如果数据范围K,比要排序的数据n大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其它类型的,要在不改变相对大小的情况下,转化为非负整数。
举个例子,如果考生成绩精确到小数点后一位,我们需要将所有的分数都先乘以10,转化成整数,然后在放到7510个桶内。再比如要排序的数据中有负数,数据的范围是[-2000,2000],那么我们需要在不改变相对大小的前提下,对每个数据都加2000,转化成非负数,范围就变成了[0,2000]。