计数排序其实是桶排序的一种特殊情况。 当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
我们都经历过高考,高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?
考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
计数排序(Counting Sort)是一种非比较排序算法,适用于一定范围内的整数排序。在计数排序中,我们创建一个临时数组(称为计数数组)来存储每个元素的出现次数。以下是一个简单的Java实现计数排序的示例:
import java.util.Arrays;
public class CountingSort {
public static void sort(int[] array) {
// 找出数组中的最大值和最小值
int max = array[0];
int min = array[0];
for (int i : array) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
// 初始化计数数组
int[] countArray = new int[max - min + 1];
// 计数每个元素的出现次数
for (int i : array) {
countArray[i - min]++;
}
// 根据计数数组排序原始数组
int arrayIndex = 0;
for (int i = 0; i < countArray.length; i++) {
while (countArray[i] > 0) {
array[arrayIndex++] = i + min;
countArray[i]--;
}
}
}
public static void main(String[] args) {
int[] data = {4, 2, 2, 8, 3, 3, 1};
sort(data);
System.out.println(Arrays.toString(data));
}
}
在这段代码中,首先通过遍历原始数组来找出最大值和最小值。之后,创建一个计数数组来统计每个元素的出现次数。计数数组的长度为原数组最大值和最小值之间的范围。
接着,再次遍历原始数组,更新计数数组中对应元素的计数。最后,根据计数数组中的计数,重新构建原始数组,从而完成排序。
计数排序的时间复杂度为O(n+k),其中n是原数组的长度,k是计数数组的长度。计数排序非常高效,特别是当k不是特别大且元素分布均匀时。然而,如果k很大(即元素范围非常宽广),计数排序可能会因为计数数组的大尺寸而变得不实用。
举例子
想象一下,你在教室里,老师让大家按身高排队。同学们的身高差不多,从最矮到最高,身高分别是:2、5、2、0、2、3、0、3 米(这是个想象的例子,所以身高可以是任意数字)。
我们的目标是快速有效地按从小到大的顺序排好队。我们可以这么做:
- 建立统计表: 老师先在黑板上画了一张表格,表格上写了从 0 到 5 的数字,每个数字代表一种身高。然后,老师让每个同学举手报告他们的身高,老师在对应身高的数字下面画一个小点表示一个学生。比如,身高为2米的学生有3个,所以在2的下面画了三个点。
- 累加统计表: 接下来,老师在每个数字下面写上了一个累积的数字。这个数字告诉我们,在排好队的队伍中,小于或等于当前身高的学生总共有多少个。例如,0 米的累积是 2,表示队伍中身高小于或等于 0 米的学生有 2 个。
- 按统计结果排队: 现在老师按照表格上的累积数字来安排队伍。首先是身高为0米的同学,累积数字告诉我们有2个人,他们站在队伍的最前面。接着是身高为2米的,有3个人,他们紧跟在身高为0米的同学后面,以此类推。
- 更新统计表: 每当一个同学站到队伍中时,老师就会在累积数字上减去1,这样就能保证下一个同学站到正确的位置上。
- 完成排队: 重复上述过程,直到所有的同学都按照身高从低到高站好为止。
在这个例子中,计数排序就像是老师使用表格的方式来快速安排队伍。老师不需要比较谁更高或者更矮,只需要按照统计好的信息来排队,这就是计数排序的高效之处。在计算机中,我们用数组来代替了黑板上的表格,用数字来代替了小点和累积的数字,其他的过程都是一样的。最终得到的队伍(或者数组)就是整齐有序的了。
那个身高排队的故事来详细解释一下图片中的步骤:
-
准备数据:
- 原始数组
A[8]代表 8 位同学的身高:{2, 5, 2, 0, 2, 3, 0, 3}。 - 数组
C[6]是老师用来记录每个身高学生数量的黑板(因为身高从0到5,所以有6个空间)。 - 数组
R[8]是最终排序好的队伍,也就是排序后的数组。
- 原始数组
-
计数各个身高的学生数量:
- 图片中的橙色条表示每个身高有多少位同学。例如,身高为0的有2人,身高为1的没有人,身高为2的有3人,以此类推。
-
计算累积计数:
- 黑板上的
C[6]数组变成了累积计数。每个数字告诉我们,包括当前身高在内的,小于等于该身高的学生总数。比如,黑板上写着2,4,7,8,这意味着有4个学生的身高是2或者更低。
- 黑板上的
-
根据累积计数进行排序:
- 每个同学根据黑板上的累积计数找到自己在队伍中的位置,这就是
R[8]的过程。 - 举个例子,身高为5的学生,他看到黑板上身高为5的累积计数是8,所以他知道自己应该站在第8位(因为我们是从1开始数的,所以在数组中对应的索引是7)。
- 每个同学根据黑板上的累积计数找到自己在队伍中的位置,这就是
-
更新累积计数并填充队伍:
- 每当一个学生站到队伍中,老师就在黑板上对应的累积计数上减1。
- 这样做是为了让下一位相同身高的学生知道他应该站在哪个位置。
- 图片中的黑箭头显示了黑板上的累积计数是如何在每个学生站好后被更新的。
-
重复直到所有学生都站好:
- 重复上述过程,直到所有学生都根据自己的身高站到了队伍中正确的位置。
图片中的每一行显示了排序过程的每一步。最后,我们得到了一个有序的队伍(R[8]),所有的学生都按照身高从矮到高站好了。
学习:极客时间《数据结构与算法之美》学习笔记