排序系列第 7 篇聚焦非比较排序:当数据范围或位数可控时,能把复杂度降到 O(n+k),但需权衡空间、稳定性与工程可行性。

目标读者

  • 处理整数键、范围/位数可知的工程师。
  • 希望用更低复杂度处理大批量数据的同学。
  • 想对比标准库比较排序与非比较排序取舍的人。

背景/动机

  • 比较排序有 Ω(n log n) 下界;非比较排序利用键范围/位数信息绕过下界,实现 O(n+k)。
  • 代价:额外空间,适用范围受限;实现需注意稳定性与内存占用。

A — Algorithm(题目与算法)

覆盖算法:计数排序、桶排序、基数排序(LSD)。

基础示例

  • 计数排序:[4, 2, 2, 8, 3],范围 0..9,计数 → 前缀和 → 稳定回填。
  • 基数排序:对整数按个位/十位/百位分组计数,逐位稳定排序。

C — Concepts(核心思想)

算法思路时间空间稳定
计数排序统计频次 + 前缀和定位O(n+k)O(k+n)可稳定
桶排序按区间分桶,桶内用其他排序期望 O(n+k)O(n+k)取决于桶内排序
基数排序按位稳定排序,多轮计数/桶O(d*(n+b))O(n+b)是(若每轮稳定)
  • k:范围大小;d:位数;b:基数(桶数)。
  • 稳定性:计数排序天然可稳定;基数排序需每轮稳定;桶排序取决于桶内算法。

E — Engineering(工程应用)

场景 1:小范围整数排序(Python 计数排序)

def counting_sort(a, max_val):
    cnt = [0]*(max_val+1)
    for x in a: cnt[x]+=1
    # 前缀和定位
    for i in range(1, len(cnt)): cnt[i]+=cnt[i-1]
    out=[0]*len(a)
    for x in reversed(a):
        cnt[x]-=1
        out[cnt[x]] = x
    return out
print(counting_sort([4,2,2,8,3], 9))

场景 2:浮点分布已知的桶排序(JavaScript)

背景:0~1 均匀分布的小数。

function bucketSort(arr, buckets=10){
  const B=Array.from({length:buckets},()=>[]);
  for(const x of arr){
    const idx = Math.min(buckets-1, Math.floor(x*buckets));
    B[idx].push(x);
  }
  for(const b of B) b.sort((a,b)=>a-b);
  return B.flat();
}
console.log(bucketSort([0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68]));

场景 3:大批量整数的基数排序(Go,LSD 基数)

package main
import "fmt"

func radixLSD(a []int) {
    maxv := 0
    for _,v := range a { if v>maxv { maxv=v } }
    exp := 1
    buf := make([]int, len(a))
    for maxv/exp > 0 {
        cnt := make([]int, 10)
        for _,v := range a { digit := (v/exp)%10; cnt[digit]++ }
        for i:=1;i<10;i++ { cnt[i]+=cnt[i-1] }
        for i:=len(a)-1;i>=0;i-- {
            d := (a[i]/exp)%10
            cnt[d]--
            buf[cnt[d]] = a[i]
        }
        copy(a, buf)
        exp *= 10
    }
}

func main(){ a:=[]int{170,45,75,90,802,24,2,66}; radixLSD(a); fmt.Println(a) }

场景 4:C++ 计数排序(小范围)

#include <bits/stdc++.h>
using namespace std;
vector<int> counting_sort(const vector<int>& a, int maxv){
    vector<int> cnt(maxv+1), out(a.size());
    for(int x:a) cnt[x]++;
    for(int i=1;i<=maxv;i++) cnt[i]+=cnt[i-1];
    for(int i=(int)a.size()-1;i>=0;i--){
        int x=a[i]; cnt[x]--; out[cnt[x]]=x;
    }
    return out;
}

场景 5:Rust 基数排序(LSD)

pub fn radix_lsd(a: &mut [u32]) {
    let mut maxv = *a.iter().max().unwrap();
    let mut exp = 1u32;
    let n = a.len();
    let mut buf = vec![0u32; n];
    while maxv/exp > 0 {
        let mut cnt = [0usize; 10];
        for &v in a.iter() { cnt[((v/exp)%10) as usize] += 1; }
        for i in 1..10 { cnt[i] += cnt[i-1]; }
        for &v in a.iter().rev() {
            let d = ((v/exp)%10) as usize;
            cnt[d] -= 1;
            buf[cnt[d]] = v;
        }
        a.copy_from_slice(&buf);
        exp *= 10;
    }
}

R — Reflection(反思与深入)

  • 复杂度与前提
    • 计数:O(n+k),k 是范围;若 k ≫ n 不合算。
    • 桶:期望 O(n+k) 取决于分布假设,最坏仍可退化。
    • 基数:O(d*(n+b)),d 为位数,b 为基数;每轮需稳定排序,常用计数。
  • 取舍
    • 内存:计数/桶需要 O(k) 或 O(n+k) 额外空间;范围大时不适用。
    • 稳定性:计数与基数可稳定,桶取决于桶内排序。
    • 数据类型:适合整数或可映射整数的键(日期、IP、定长字符串)。
  • 为何可行
    • 当范围/位数可控时,非比较排序打破 n log n 下界,显著提速;
    • 在日志分桶、分段统计、批量整数排序等场景表现优异。

S — Summary(总结)

  • 非比较排序依赖“已知范围/位数/分布”前提,能实现 O(n+k) 时间。
  • 计数排序简单稳定,适合小范围整数;基数排序适合多位整数/定长键;桶排序依赖分布假设。
  • 核心风险:空间占用、分布假设不成立、稳定性需求未满足。
  • 选型:范围小 → 计数;位数适中、需稳定 → 基数;均匀分布浮点 → 桶;否则回到比较排序。

实践指南 / 步骤

  • 先估算范围/位数:若 k 接近 n 甚至更大,谨慎使用计数。
  • 明确稳定性:基数需每轮稳定排序;桶内如需稳定,选稳定算法。
  • 控制内存:计数数组长度 = max-min+1;基数的缓冲至少 O(n)。
  • 准备测试:随机、全相等、范围极大、分布偏斜,评估性能与内存。

常见问题与注意事项

  • 计数排序忘记偏移处理负数:需平移或分正负两段。
  • 基数排序每轮若用不稳定排序,会破坏最终稳定性。
  • 桶排序在分布偏斜时退化,可增加桶数或对大桶再用非比较/比较排序混合。
  • 内存过大时需改用比较排序或分块处理。

可运行示例:Python 负数计数排序(带偏移)

def counting_sort_with_neg(a):
    mn, mx = min(a), max(a)
    offset = -mn
    cnt = [0]*(mx - mn + 1)
    for x in a: cnt[x+offset]+=1
    for i in range(1,len(cnt)): cnt[i]+=cnt[i-1]
    out=[0]*len(a)
    for x in reversed(a):
        cnt[x+offset]-=1
        out[cnt[x+offset]] = x
    return out
print(counting_sort_with_neg([3,-1,2,-1,0]))

参考与延伸阅读

  • CLRS《算法导论》非比较排序章节
  • Donald Knuth, “The Art of Computer Programming, Vol. 3”(排序与查找)
  • 关于整数排序下界与模型假设的讨论(word-RAM 模型)

元信息

  • 阅读时长:约 15 分钟
  • SEO 关键词:计数排序, 桶排序, 基数排序, 非比较排序, O(n+k)
  • 元描述:排序专题第七篇,讲解非比较排序的适用前提、复杂度与工程实现,附多语言示例与取舍建议。

行动号召(CTA)

  • 为你的数据估算范围/位数,尝试实现一版计数或基数排序并基准测试。
  • 若分布偏斜,试调桶数或在大桶内改用基数/比较排序,记录效果。
  • 关注后续系列:TimSort/Introsort 与排序选型实战篇。