一、运动会的故事:如何快速统计奖牌数量
学校运动会结束后,需要统计各个班级获得的奖牌数量(金牌、银牌、铜牌)。传统方法是一个一个数,效率很低。体育老师想到了一个办法:
-
准备三个盒子:分别标上 "金牌"、"银牌"、"铜牌"
-
分类投放:让每个班级把奖牌对应放入三个盒子中
-
统计结果:直接数每个盒子里的奖牌数量,得到最终排名
这就是计数排序的核心思想:通过统计每个元素的出现次数来实现排序。
二、计数排序的 Java 代码实现
java
ini
public class CountingSort {
public static void sort(int[] array) {
if (array == null || array.length == 0) return;
// 1. 找出数组中的最大值和最小值
int max = array[0];
int min = array[0];
for (int num : array) {
if (num > max) max = num;
if (num < min) min = num;
}
// 2. 创建计数数组,大小为(max - min + 1)
int[] countArray = new int[max - min + 1];
// 3. 统计每个元素出现的次数
for (int num : array) {
countArray[num - min]++; // 偏移量为min,确保数组索引从0开始
}
// 4. 根据计数数组重构原数组
int index = 0;
for (int i = 0; i < countArray.length; i++) {
while (countArray[i] > 0) {
array[index++] = i + min; // 还原实际值
countArray[i]--;
}
}
}
}
三、计数排序的核心步骤解析
1. 确定范围
找出数组中的最大值和最小值,确定计数数组的大小:
java
ini
// 示例数组:[4, 2, 2, 8, 3, 3, 1]
max = 8, min = 1
计数数组大小 = 8 - 1 + 1 = 8
2. 统计次数
遍历原数组,统计每个元素出现的次数:
java
arduino
// 计数数组初始状态:[0, 0, 0, 0, 0, 0, 0, 0]
// 遍历后:[1, 2, 2, 0, 1, 0, 0, 1]
// 索引对应值: 1 2 3 4 5 6 7 8
3. 重构数组
根据计数数组的结果,重新填充原数组:
java
arduino
// 重构过程:
// 索引0值为1,原数组填充1个1 → [1]
// 索引1值为2,原数组填充2个2 → [1, 2, 2]
// 索引2值为2,原数组填充2个3 → [1, 2, 2, 3, 3]
// 索引3值为0,跳过
// 索引4值为1,原数组填充1个4 → [1, 2, 2, 3, 3, 4]
// ...
// 最终结果:[1, 2, 2, 3, 3, 4, 8]
四、计数排序的性能分析
-
时间复杂度:O (n + k),其中 n 是元素个数,k 是数值范围
-
空间复杂度:O (k),主要用于计数数组
-
稳定性:可以实现稳定排序(通过改进算法)
-
适用条件:
- 数据范围较小(k 远小于 n)
- 非负整数(或可映射到非负整数)
五、计数排序的优化与变种
1. 稳定计数排序
在重构数组时,从后向前遍历原数组,可以保证相同元素的相对顺序不变:
java
ini
public static void stableSort(int[] array) {
if (array == null || array.length == 0) return;
// 找出最大值和最小值
int max = array[0], min = array[0];
for (int num : array) {
if (num > max) max = num;
if (num < min) min = num;
}
// 创建计数数组
int[] countArray = new int[max - min + 1];
// 统计次数
for (int num : array) {
countArray[num - min]++;
}
// 计算前缀和(用于确定元素在结果数组中的位置)
for (int i = 1; i < countArray.length; i++) {
countArray[i] += countArray[i - 1];
}
// 创建临时数组,保存排序结果
int[] temp = new int[array.length];
for (int i = array.length - 1; i >= 0; i--) {
int num = array[i];
int position = countArray[num - min] - 1;
temp[position] = num;
countArray[num - min]--;
}
// 复制回原数组
System.arraycopy(temp, 0, array, 0, array.length);
}
2. 处理负数
如果数组包含负数,可以通过偏移量将所有数映射为非负数:
java
arduino
// 示例:处理包含负数的数组 [-3, 2, -1, 0, 5]
// 最小值为-3,偏移量为3
// 映射后:[0, 5, 2, 3, 8]
// 排序后再映射回原数值
六、计数排序 vs 其他排序算法
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|
计数排序 | O(n + k) | O(k) | 稳定 | 数据范围小的整数排序 |
快速排序 | O(n log n) | O(log n) | 不稳定 | 通用场景 |
归并排序 | O(n log n) | O(n) | 稳定 | 大规模数据、链表排序 |
插入排序 | O(n²) | O(1) | 稳定 | 小规模或基本有序数据 |
七、总结:计数排序的核心思想
计数排序的本质是 "用空间换时间":
-
统计次数:用一个数组记录每个元素出现的次数
-
重构数组:根据统计结果,按顺序填充原数组
就像运动会统计奖牌,通过分类投放和直接计数,快速得到结果。计数排序特别适合处理数据范围较小的整数排序,效率远超比较类排序算法(如快速排序、归并排序)。但它的局限性在于需要额外空间存储计数数组,且不适用于浮点数、字符串等复杂数据类型。