第一次听说基数排序时,我完全无法理解不比较数字大小怎么排序。直到亲眼看到它处理10万个手机号排序的场景,速度比快速排序还快3倍,才明白这种"按位分治"的独特魅力。
基数排序就像整理图书馆的书籍编号:先按第一个字母分区域(A区、B区...),每个区域内再按第二个字母细分。这种思路用在数字处理上,就是按数字的每一位进行分组排序。实际项目中我常用它处理:
与常见的比较排序不同,它的时间复杂度能达到O(n),但需要额外空间。下面这张表对比了几种常见排序算法:
| 算法类型 | 平均时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 快速排序 | O(nlogn) | O(logn) | 不稳定 | 通用场景 |
| 归并排序 | O(nlogn) | O(n) | 稳定 | 链表排序 |
| 基数排序 | O(n*k) | O(n+k) | 稳定 | 整数排序 |
提示:k代表数字最大位数,当k远小于n时,基数排序优势明显
上周团队新来的实习生问我:"为什么叫最低位优先?"我用超市存包柜的例子解释:假设柜子编号从00到99,我们整理包裹时:
这个过程就是LSD(Least Significant Digit)的生动体现。来看具体步骤:
python复制max_num = max(arr)
max_digit = len(str(max_num)) # 最大位数
python复制for digit in range(max_digit):
# 创建10个桶(0-9)
buckets = [[] for _ in range(10)]
# 分配数字到桶中
for num in arr:
radix = (num // (10**digit)) % 10
buckets[radix].append(num)
# 按桶顺序重组数组
arr = [num for bucket in buckets for num in bucket]
第一次实现LSD时,我犯了个典型错误:每轮都创建新桶,导致内存激增。后来优化为复用桶空间:
java复制// 优化后的Java实现
public static void lsdSort(int[] arr) {
final int RADIX = 10;
int[][] buckets = new int[RADIX][arr.length];
int[] counts = new int[RADIX];
int max = Arrays.stream(arr).max().getAsInt();
int digitCount = String.valueOf(max).length();
for (int d = 0; d < digitCount; d++) {
// 分配阶段
for (int num : arr) {
int radix = (num / (int)Math.pow(10, d)) % RADIX;
buckets[radix][counts[radix]++] = num;
}
// 收集阶段
int idx = 0;
for (int k = 0; k < RADIX; k++) {
for (int i = 0; i < counts[k]; i++) {
arr[idx++] = buckets[k][i];
}
counts[k] = 0; // 清空计数器
}
}
}
注意:当处理负数时,需要先将所有数加上最小值的绝对值转为非负数,排序后再转换回去
去年优化一个地名排序系统时,我发现LSD对长短不一的字符串效率低下。改用MSD(Most Significant Digit)后性能提升40%,它的核心思想是:
这个过程类似文件系统的目录结构:
这是我优化过的MSD实现,关键点在于:
python复制def msd_sort(arr, radix):
if len(arr) <= 1 or radix == 0:
return arr
# 初始化桶
buckets = [[] for _ in range(10)]
# 分配元素
for num in arr:
pos = (num // radix) % 10
buckets[pos].append(num)
# 递归处理非空桶
result = []
for bucket in buckets:
if not bucket:
continue
if radix // 10 > 0 and len(bucket) > 1:
result += msd_sort(bucket, radix // 10)
else:
result += bucket
return result
实际项目中,当数据位数差异大时(如同时存在5位数和8位数ID),MSD相比LSD能减少不必要的低位排序操作。
在百万级数据集测试中,我发现:
| 数据特征 | LSD耗时 | MSD耗时 | 内存占用 |
|---|---|---|---|
| 固定8位数字 | 1.2s | 1.5s | LSD少15% |
| 位数3-8位不等 | 2.8s | 1.1s | MSD少40% |
| 包含大量重复前缀 | 1.5s | 0.9s | 基本持平 |
关键结论:
根据实战经验,我总结出选择原则:
code复制是否需要排序数字?
├─ 是 → 数据位数是否固定?
│ ├─ 是 → 选择LSD
│ └─ 否 → 数据量是否大于1万?
│ ├─ 是 → 选择MSD
│ └─ 否 → 两者均可
└─ 否 → 考虑其他排序算法
特殊场景处理建议:
第一次处理千万级数据时,程序因为bucket数组过大直接OOM。后来采用以下优化:
优化后的Java片段:
java复制// 使用ByteBuffer做磁盘缓存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024*1024);
for (int num : currentBucket) {
buffer.putInt(num);
if (!buffer.hasRemaining()) {
flushToTempFile(buffer);
}
}
在电商订单排序时发现,原本稳定的基数排序在优化后出现错乱。原因是:
修正方法:
python复制# 正确保持稳定性的收集方式
output = []
for bucket in buckets:
output.extend(bucket) # 保持桶内原始顺序
金融数据常包含负数,我的处理方案是:
cpp复制int min_val = *min_element(arr.begin(), arr.end());
int offset = abs(min_val);
for (int& num : arr) num += offset;
// ...排序操作...
for (int& num : arr) num -= offset;
这些经验都来自真实项目中的教训,现在团队新人上手前我都会让他们先看这段防坑指南。