计数排序故事:运动会的奖牌统计

一、运动会的故事:如何快速统计奖牌数量

学校运动会结束后,需要统计各个班级获得的奖牌数量(金牌、银牌、铜牌)。传统方法是一个一个数,效率很低。体育老师想到了一个办法:

  1. 准备三个盒子:分别标上 "金牌"、"银牌"、"铜牌"

  2. 分类投放:让每个班级把奖牌对应放入三个盒子中

  3. 统计结果:直接数每个盒子里的奖牌数量,得到最终排名

这就是计数排序的核心思想:通过统计每个元素的出现次数来实现排序

二、计数排序的 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) 稳定 小规模或基本有序数据

七、总结:计数排序的核心思想

计数排序的本质是 "用空间换时间":

  1. 统计次数:用一个数组记录每个元素出现的次数

  2. 重构数组:根据统计结果,按顺序填充原数组

就像运动会统计奖牌,通过分类投放和直接计数,快速得到结果。计数排序特别适合处理数据范围较小的整数排序,效率远超比较类排序算法(如快速排序、归并排序)。但它的局限性在于需要额外空间存储计数数组,且不适用于浮点数、字符串等复杂数据类型。

相关推荐
雨白1 小时前
Drawable 与 Bitmap 的区别、互转与自定义
android
程序员江同学1 小时前
Kotlin 技术月报 | 2025 年 8 月
android·kotlin
nju永远得不到的男人2 小时前
关于virtual camera
android
雨白4 小时前
Android 自定义 View:属性动画和硬件加速
android
hellokai6 小时前
React Native新架构源码分析
android·前端·react native
真西西6 小时前
Koin:Kotlin轻量级依赖注入框架
android·kotlin
CYRUS_STUDIO8 小时前
手把手教你改造 AAR:解包、注入逻辑、重打包,一条龙玩转第三方 SDK!
android·逆向
CYRUS_STUDIO9 小时前
Android 源码如何导入 Android Studio?踩坑与解决方案详解
android·android studio·源码阅读
前端赵哈哈10 小时前
初学者入门:Android 实现 Tab 点击切换(TabLayout + ViewPager2)
android·java·android studio
一条上岸小咸鱼13 小时前
Kotlin 控制流(二):返回和跳转
android·kotlin