基数排序故事:扑克牌的分级整理

200 阅读4分钟

一、扑克牌的故事:如何快速整理一副牌

小明拿到了一副打乱的扑克牌,需要按照 "花色 + 数字" 的顺序整理好。他想到了一个办法:

  1. 第一次分组:先按照花色(红桃、方块、黑桃、梅花)分成四堆

  2. 第二次分组:对每堆牌,再按照数字(A, 2-10, J, Q, K)进行排序

  3. 合并结果:按顺序合并所有牌堆

这就是基数排序的核心思想:按位分组,逐步细化

二、基数排序的 Java 代码实现

java

public class RadixSort {
    public static void sort(int[] array) {
        if (array == null || array.length == 0) return;
        
        // 1. 找出最大值,确定最大位数
        int max = array[0];
        for (int num : array) {
            if (num > max) max = num;
        }
        
        // 2. 从个位到最高位,逐位排序
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countingSortByDigit(array, exp);
        }
    }
    
    // 按位进行计数排序(稳定排序)
    private static void countingSortByDigit(int[] array, int exp) {
        int n = array.length;
        int[] output = new int[n];
        int[] count = new int[10];  // 0-9共10个数字
        
        // 1. 统计当前位的出现次数
        for (int i = 0; i < n; i++) {
            int digit = (array[i] / exp) % 10;
            count[digit]++;
        }
        
        // 2. 计算前缀和,确定元素位置
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }
        
        // 3. 从后向前遍历,保证排序稳定性
        for (int i = n - 1; i >= 0; i--) {
            int digit = (array[i] / exp) % 10;
            output[count[digit] - 1] = array[i];
            count[digit]--;
        }
        
        // 4. 复制回原数组
        System.arraycopy(output, 0, array, 0, n);
    }
}

三、基数排序的核心步骤解析

1. 确定排序位数

找出数组中的最大值,确定需要处理的最大位数:

java

// 示例数组:[170, 45, 75, 90, 802, 24, 2, 66]
max = 802 → 最大位数为3(百位)
2. 按位进行计数排序

从最低位(个位)开始,逐位进行稳定排序:

java

// 第一次排序(个位):
// 原数组:[170, 45, 75, 90, 802, 24, 2, 66]
// 按个位排序后:[170, 90, 802, 2, 24, 45, 75, 66]

// 第二次排序(十位):
// 按十位排序后:[802, 2, 24, 45, 66, 170, 75, 90]

// 第三次排序(百位):
// 按百位排序后:[2, 24, 45, 66, 75, 90, 170, 802] → 最终结果
3. 稳定排序的重要性

基数排序依赖稳定排序保证每一步的结果正确传递到下一步。例如:

java

// 第二次排序(十位)时,802和2的十位都是0
// 由于第一次排序后802在前,稳定排序会保持这个顺序
// 因此第二次排序后:[802, 2, ...],而不是[2, 802, ...]

四、基数排序的性能分析

  • 时间复杂度:O(d × (n + k))

    • d:最大位数
    • n:元素个数
    • k:每一位的取值范围(通常为 10)
  • 空间复杂度:O(n + k)

  • 稳定性:稳定

  • 适用条件

    • 整数或可转换为整数的数据(如字符串按字符编码)
    • 位数较少(d 较小)

五、基数排序的优化与变种

1. 处理负数

将所有数加上一个偏移量,转换为非负数:

java

// 示例:处理包含负数的数组 [-123, 45, -6, 789]
// 偏移量 = 123
// 转换后:[0, 168, 117, 912]
// 排序后再减去偏移量还原
2. 字符串排序

按字符编码逐位排序,从最低有效位(LSD)到最高有效位(MSD):

java

// 示例:排序 ["apple", "banana", "cherry"]
// 按最后一个字符排序:["cherry", "apple", "banana"]
// 按倒数第二个字符排序:["apple", "banana", "cherry"]
// ...

六、基数排序 vs 其他排序算法

排序算法时间复杂度空间复杂度稳定性适用场景
基数排序O(d × (n + k))O(n + k)稳定整数或字符串排序
快速排序O(n log n)O(log n)不稳定通用场景
归并排序O(n log n)O(n)稳定大规模数据、链表排序
计数排序O(n + k)O(k)稳定数据范围小的整数排序

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

基数排序的本质是 "逐位分解,按位排序":

  1. 按位分组:从最低位到最高位,依次对每一位进行排序

  2. 稳定传递:依赖稳定排序保证前一步的结果不被破坏

就像整理扑克牌,先按花色分堆,再按数字细化。基数排序特别适合处理位数较少的整数或字符串排序,效率高于基于比较的排序算法。但它需要额外空间存储临时数组,且不适用于无法转换为固定位数的数据类型。