1-18-基数排序-RadixSort

基数排序 (Radix Sort):按位多轮计数排序

摘要:本文从"计数排序只能排小范围整数,大整数怎么办"的问题出发,详解基数排序如何通过"将大整数分解为多个位,每位独立计数排序"的思路,将非比较排序的适用范围扩展到任意位数的整数甚至字符串。给出了 LSD(最低位优先)、MSD(最高位优先)、含负数版、字符串版共四个 Python 完整实现,图解了逐位计数排序的稳定传递过程,分析了基数选择对性能的影响。最后结合后缀数组、字符串匹配等工程场景讨论其设计哲学与面试高频考点。
本文属于专栏《算法》系列 1 第 18 篇 | 上一篇:桶排序 (Bucket Sort) | 下一篇:(待更新)


文章目录

一、问题引入

上一篇我们讨论了桶排序------一种利用"数据分布"的非比较排序,平均时间 O(n)。但桶排序有一个局限:性能依赖数据分布的均匀性。如果数据集中在一个桶里,就退化为 O(n²)。

有没有一种非比较排序,不依赖数据分布,只要数据是"可按位分解"的整数就能稳定地做到线性时间?

答案是:基数排序

计数排序的局限

计数排序只能处理取值范围 k 较小的数据。如果数据是 32 位整数(范围 0~2³²),计数数组需要 4GB 内存------完全不可行。

但如果换个角度思考:一个 32 位整数可以看作 8 个 4 位十六进制数,或 10 个十进制数字。如果按每一位独立做计数排序,每位的范围只有 10(十进制)或 16(十六进制),计数数组只需要 10 或 16 个位置。

这就是基数排序的核心思路:将"排大整数"分解为"排多次小范围的位"

一个直觉例子

要对 [170, 45, 75, 90, 802, 24, 2, 66] 排序,不需要计数排序处理整个数值范围(0~802),而是:

  1. 按个位排序:0, 0, 2, 2, 4, 5, 5, 6
  2. 按十位排序:0, 2, 4, 6, 7, 7, 9, ?
  3. 按百位排序:0, 0, 0, 0, 0, 0, 1, 8

每轮只用 10 个桶(十进制 0~9),三轮就排完了。这就是基数排序------多轮计数排序的稳定传递

算法 核心思路 适用数据 时间复杂度
计数排序 统计每个值的次数 小范围整数 O(n + k)
桶排序 按区间分桶,桶内排序 均匀分布数值 平均 O(n)
基数排序 按位多轮计数排序 整数、字符串 O(d·(n + base))

问题定义:

  • 输入 :含 n 个整数的数组 arr(或字符串数组)
  • 输出:按升序(或降序)排列的数组
  • 核心约束:数据可按位分解(每一位的范围有限)
  • 核心操作:逐位计数排序,从低位到高位(LSD)或从高位到低位(MSD)

二、算法原理图解

核心思想

基数排序(Radix Sort)的核心是将多"位"数据的排序分解为单"位"排序的序列。每位用稳定的计数排序处理,低位排序结果在高位排序时通过稳定性保持不变。

基数排序有两种策略:

  • LSD(Least Significant Digit):从最低位开始,逐位向高位推进
  • MSD(Most Significant Digit):从最高位开始,按当前位分桶后递归处理低位

LSD 基数排序图解

以数组 [170, 045, 075, 090, 802, 024, 002, 066] 升序排序为例:

第 1 轮:按个位排序

复制代码
原始:    [170, 045, 075, 090, 802, 024, 002, 066]
个位:      0    5    5    0    2    4    2    6

计数排序(按个位)→ 稳定排序 →
第1轮:   [170, 090, 802, 002, 024, 045, 075, 066]
个位:      0    0    2    2    4    5    5    6
          ↑---------------↑↑-----↑↑-----↑
          个位0个位2个位4个位5个位6,组内保持原始顺序

第 2 轮:按十位排序

复制代码
当前:    [170, 090, 802, 002, 024, 045, 075, 066]
十位:      7    9    0    0    2    4    7    6

计数排序(按十位)→ 稳定排序 →
第2轮:   [802, 002, 024, 045, 066, 170, 075, 090]
十位:      0    0    2    4    6    7    7    9
          ↑-----↑↑---↑↑---↑↑---↑↑-----↑↑---↑
          十位0 十位2 十位4 十位6 十位7 十位9

第 3 轮:按百位排序

复制代码
当前:    [802, 002, 024, 045, 066, 170, 075, 090]
百位:      8    0    0    0    0    1    0    0

计数排序(按百位)→ 稳定排序 →
第3轮:   [002, 024, 045, 066, 075, 090, 170, 802]
百位:      0    0    0    0    0    0    1    8
          ↑---------------------↑↑-----↑↑-----↑
          百位0(6个)       百位1  百位8

完成! 结果 [2, 24, 45, 66, 75, 90, 170, 802] 完全有序。

为什么低位排序的结果能保持?

这是基数排序最核心的问题:为什么从低位开始排序,最终结果是正确的?

关键在于计数排序的稳定性。以第 1 轮和第 2 轮为例:

复制代码
第1轮结果(按个位排序后):
  [170, 090, 802, 002, 024, 045, 075, 066]

第2轮按十位排序时,170 和 075 的十位都是 7:
  170 的十位 = 7
  075 的十位 = 7

因为计数排序是稳定的,所以 170(在第1轮结果中排在 075 前面)
在第2轮排序后仍然排在 075 前面。

而 170 的个位是 0,075 的个位是 5。
个位 0 < 个位 5 → 170 应该在 075 前面 ✓

所以:低位排序的相对顺序通过稳定性被保留,
高位排序只决定"大区间"的顺序,小区间内由低位排序结果决定。

总结 :LSD 基数排序的正确性依赖于一个关键性质------如果两个元素的高位相同,它们的相对顺序由低位排序结果决定。计数排序的稳定性恰好保证了这一点。

MSD 基数排序图解

MSD(最高位优先)的思路完全不同------从最高位开始分桶,每个桶递归处理低位:

复制代码
原始: [170, 045, 075, 090, 802, 024, 002, 066]

第1轮(按百位分桶):
  桶0: [045, 075, 090, 024, 002, 066]  ← 百位=0
  桶1: [170]                            ← 百位=1
  桶8: [802]                            ← 百位=8

桶0递归(按十位分桶):
  桶0: [002]       ← 十位=0
  桶2: [024]       ← 十位=2
  桶4: [045]       ← 十位=4
  桶6: [066]       ← 十位=6
  桶7: [075]       ← 十位=7
  桶9: [090]       ← 十位=9

(桶1、桶8只有一个元素,无需递归)

桶0桶0递归(按个位):只有 [002],无需递归。

最终按桶顺序拼接:
  [002, 024, 045, 066, 075, 090, 170, 802]

LSD vs MSD 对比

维度 LSD MSD
方向 低位 → 高位 高位 → 低位
实现 迭代(无需递归) 递归
位数 所有位都要处理 可以提前终止(单元素桶)
适用场景 等长数据(整数) 变长数据(字符串)
缓存友好 更好(顺序访问) 较差(递归跳转)

基数的选择

基数(base)是基数排序的关键参数------它决定了"每位有多少种值"以及"需要多少轮排序"。

以 32 位整数为例:

基数 每位范围 轮数 d 每轮桶数 总操作量
2 0~1 32 2 32×(n+2)
10 0~9 10 10 10×(n+10)
16 0~15 8 16 8×(n+16)
256 0~255 4 256 4×(n+256)
65536 0~65535 2 65536 2×(n+65536)

基数的选择是一个权衡:

  • 基数大:轮数少(d 小),但每轮的桶多(base 大),空间开销大
  • 基数小:桶少(空间省),但轮数多(d 大),每轮都要遍历整个数组

理论上存在一个最优基数,使 d × (n + base) 最小。实际中,base = 256 或 base = 65536(按字节/双字节处理)在 C/C++ 中很常见。


三、代码实现

完整代码

通过网盘分享的文件:算法

链接: https://pan.baidu.com/s/1DTJt1X2Is_IQeH5fAXvtZg?pwd=yyqf 提取码: yyqf

--来自百度网盘超级会员v4的分享

LSD 版:标准基数排序

python 复制代码
def radix_sort_lsd(arr, ascending=True, base=10):
    """
    基数排序(LSD:最低位优先):从最低位开始,逐位用计数排序排序。

    核心思想:
    1. 将整数按基数(如十进制 base=10)分解为各位数字
    2. 从最低位(个位)开始,用稳定计数排序按当前位排序
    3. 逐位向高位推进,直到处理完最高位
    4. 由于计数排序是稳定的,低位排序结果在高位排序时保持不变

    时间复杂度:O(d·(n + base))  d 为最大位数  |  空间复杂度:O(n + base)  |  稳定排序
    """
    if len(arr) <= 1:
        return list(arr)

    if any(v < 0 for v in arr):
        return radix_sort_signed(arr, ascending, base)

    max_val = max(arr)
    if max_val == 0:
        return list(arr)

    result = list(arr)

    # 逐位排序:exp = 1(个位), base(十位), base²(百位)...
    exp = 1
    while max_val // exp > 0:
        result = _counting_sort_by_digit(result, exp, base, ascending)
        exp *= base

    return result

六个关键设计解析

设计1:逐位提取数字

python 复制代码
digit = (val // exp) % base

如何提取第 exp 位的数字? 这行代码做了两步操作:

  • val // exp:右移 exp 位,使目标位变成最低位
  • % base:取最低位数字

例如 val = 802, exp = 100, base = 10

  • 802 // 100 = 8 → 右移两位,百位变成个位
  • 8 % 10 = 8 → 取出百位数字 8

这是基数排序最核心的操作------从任意位置提取一个"位"的值

设计2:轮数由最大值决定

python 复制代码
max_val = max(arr)
exp = 1
while max_val // exp > 0:
    result = _counting_sort_by_digit(result, exp, base, ascending)
    exp *= base

为什么要找最大值? 基数排序需要处理到"最高非零位"。最大值决定了最高位在哪------如果有 n 位数,就需要 n 轮排序。max_val // exp > 0 的含义是"max_val 还有未处理的位"。

设计3:每轮用稳定计数排序

python 复制代码
def _counting_sort_by_digit(arr, exp, base, ascending):
    """计数排序:按第 exp 位数字排序。"""
    n = len(arr)
    result = [None] * n
    count = [0] * base

    for val in arr:
        digit = (val // exp) % base
        count[digit] += 1

    if ascending:
        for i in range(1, base):
            count[i] += count[i - 1]
    else:
        for i in range(base - 2, -1, -1):
            count[i] += count[i + 1]

    # 从后往前放置(保证稳定性)
    for i in range(n - 1, -1, -1):
        val = arr[i]
        digit = (val // exp) % base
        count[digit] -= 1
        result[count[digit]] = val

    return result

为什么必须用稳定的计数排序? 如果计数排序不稳定,高位排序时相同"位值"的元素的相对顺序会被打乱,低位排序的结果就白费了。稳定性是基数排序正确性的根基

计数排序的稳定性来自"从后往前遍历 + 前缀和右边界"的组合------上一篇计数排序文章已详细分析。

设计4:LSD 用迭代而非递归

python 复制代码
exp = 1
while max_val // exp > 0:
    result = _counting_sort_by_digit(result, exp, base, ascending)
    exp *= base

LSD 为什么不需要递归? LSD 从最低位开始,每一位的排序都是对整个数组的完整计数排序。每轮的结果是下一轮的输入,不需要像 MSD 那样"分桶后递归处理每个桶"。这使得 LSD 的实现更简洁,且缓存更友好(每轮顺序扫描整个数组)。

设计5:MSD 用递归分桶

python 复制代码
def _msd_sort_recursive(arr, exp, base, ascending):
    if exp == 0 or len(arr) <= 1:
        return arr

    # 按 exp 位分桶
    buckets = [[] for _ in range(base)]
    for val in arr:
        digit = (val // exp) % base
        buckets[digit].append(val)

    # 递归处理每个桶的低位
    result = []
    order = range(base) if ascending else range(base - 1, -1, -1)
    for d in order:
        if len(buckets[d]) > 1:
            buckets[d] = _msd_sort_recursive(buckets[d], exp // base, base, ascending)
        result.extend(buckets[d])

    return result

MSD 为什么要递归? MSD 从最高位开始分桶。分桶后,同一个桶内的元素高位相同,但低位可能不同------需要对每个桶递归处理低位。这种"分桶 → 递归"的结构天然适合递归实现。

MSD 的提前终止优势:如果某个桶只有一个元素,可以立即返回------不需要处理所有位数。对于有大量重复前缀的数据(如字符串),MSD 可以比 LSD 少处理很多位。

设计6:负数的处理策略

python 复制代码
def radix_sort_signed(arr, ascending=True, base=10):
    """将负数和正数分开处理。"""
    negatives = [-v for v in arr if v < 0]
    positives = [v for v in arr if v >= 0]

    negatives = radix_sort_lsd(negatives, ascending=True, base=base)
    positives = radix_sort_lsd(positives, ascending=True, base=base)

    if ascending:
        # 升序:负数(绝对值大的在前 → 反转后取负)+ 正数
        return [-v for v in reversed(negatives)] + positives
    else:
        return list(reversed(positives)) + [-v for v in negatives]

为什么不能直接对负数做基数排序? 负数的二进制表示中,符号位为 1,按位排序会产生错误结果(负数会被排到正数后面)。

解决策略:将负数取绝对值,变成正数后排好序,最后再取负数并反转。升序时,绝对值大的负数更小(-8 < -2),所以负数部分反转后取负;正数部分直接拼接。

MSD 版

python 复制代码
def radix_sort_msd(arr, ascending=True, base=10):
    """基数排序(MSD:最高位优先):从最高位开始,递归分桶排序。"""
    if len(arr) <= 1:
        return list(arr)

    if any(v < 0 for v in arr):
        return radix_sort_signed(arr, ascending, base)

    max_val = max(arr)
    if max_val == 0:
        return list(arr)

    # 计算最高位的指数
    max_exp = 1
    while max_val // (max_exp * base) > 0:
        max_exp *= base

    return _msd_sort_recursive(list(arr), max_exp, base, ascending)

字符串版

python 复制代码
def radix_sort_string(arr, ascending=True):
    """基数排序(字符串版):从最后一个字符开始,逐字符用计数排序。"""
    if len(arr) <= 1:
        return list(arr)

    max_len = max(len(s) for s in arr)
    padded = [s.ljust(max_len, '\0') for s in arr]

    result = list(padded)

    # 从最后一个字符开始,逐字符计数排序
    for pos in range(max_len - 1, -1, -1):
        result = _counting_sort_by_char(result, pos, ascending)

    return [s.rstrip('\0') for s in result]

字符串版说明:字符串排序是 LSD 基数排序的经典应用。不等长的字符串在末尾补 '\0'(ASCII 最小),从最后一个字符开始逐字符计数排序。'\0' 是最小的字符,所以短字符串会排在前面------这符合字典序。

运行验证

python 复制代码
if __name__ == "__main__":
    data = [170, 45, 75, 90, 802, 24, 2, 66]
    print(f"排序前: {data}")
    print(f"升序(LSD): {radix_sort_lsd(data[:])}")
    print(f"降序(LSD): {radix_sort_lsd(data[:], ascending=False)}")
    print(f"升序(MSD): {radix_sort_msd(data[:])}")
    print(f"降序(MSD): {radix_sort_msd(data[:], ascending=False)}")

    # 含负数
    neg_data = [-5, 3, -2, 0, 7, -1, 3, -8]
    print(f"含负数升序: {radix_sort_signed(neg_data[:])}")

    # 字符串排序
    str_data = ["banana", "apple", "cherry", "date", "fig", "grape"]
    print(f"字符串升序: {radix_sort_string(str_data[:])}")

输出:

复制代码
排序前: [170, 45, 75, 90, 802, 24, 2, 66]
升序(LSD): [2, 24, 45, 66, 75, 90, 170, 802]
降序(LSD): [802, 170, 90, 75, 66, 45, 24, 2]
升序(MSD): [2, 24, 45, 66, 75, 90, 170, 802]
降序(MSD): [802, 170, 90, 75, 66, 45, 24, 2]

含负数升序: [-8, -5, -2, -1, 0, 3, 3, 7]

字符串升序: ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']

验证说明:以上输出确认了 LSD 和 MSD 两种策略结果一致,负数排序正确,字符串按字典序排序正确。50 轮随机测试(含负数)全部通过,验证了算法的正确性。


四、复杂度分析

时间复杂度

情况 复杂度 说明
最好 O(d·(n + base)) 与数据分布无关,总是 d 轮计数排序
平均 O(d·(n + base)) 稳定的线性时间
最坏 O(d·(n + base)) 没有最坏情况(非比较排序)

其中 d 为最大位数,base 为基数。

推导过程

复制代码
LSD 基数排序:
  轮数:d = ⌈log_base(max_val)⌉ + 1
  每轮计数排序:O(n + base)
  总计:d × O(n + base) = O(d·(n + base))

当 base 是常数(如 10)时:
  d ≈ log₁₀(max_val) ≈ log₁₀(n)  (假设 max_val 和 n 同阶)
  总计:O(log n × n) = O(n log n)

当 base = n 时:
  d ≈ log_n(max_val) ≈ 常数(通常 2~3)
  总计:O(常数 × (n + n)) = O(n)  ← 真正的线性时间

关键结论:当基数 base 与 n 同阶时,基数排序是真正的线性时间 O(n)。但代价是空间 O(n + base) = O(n)。这是用空间换时间的典型案例。

空间复杂度

部分 空间 说明
计数数组 O(base) 每轮的桶计数
输出数组 O(n) 每轮的排序结果
总计 O(n + base) 非原地排序

稳定性

稳定排序。基数排序的稳定性来自计数排序的稳定性。每一轮的计数排序都是稳定的,所以低位排序的相对顺序在高位排序中被保留。

稳定性的传递链

  1. 第 1 轮(个位):计数排序稳定 → 相同个位的元素保持原始顺序
  2. 第 2 轮(十位):计数排序稳定 → 相同十位的元素保持第 1 轮的顺序
  3. ...
  4. 第 d 轮(最高位):计数排序稳定 → 相同最高位的元素保持第 d-1 轮的顺序

如果任意一轮的排序不稳定,整个基数排序就不正确。

基数排序 vs 计数排序 vs 桶排序

维度 计数排序 桶排序 基数排序
思路 统计每个值次数 按区间分桶 按位多轮计数
适用数据 整数、范围小 数值、均匀分布 整数、字符串
时间 O(n + k) 平均O(n) 最坏O(n²) O(d·(n+base))
最坏退化 不退化 退化为O(n²) 不退化
空间 O(n + k) O(n + k) O(n + base)
稳定性 稳定 可稳定 稳定
数据分布依赖 不依赖 严重依赖 不依赖

三者关系:计数排序是桶排序的极端情况(每桶一个值),基数排序是多轮计数排序的串联。它们同属非比较排序家族,但各有适用场景。


五、横向对比

性能对比验证

python 复制代码
import time
import random

print("--- 性能对比 (n=10000, 范围0~99999) ---")
perf_data = [random.randint(0, 99999) for _ in range(10000)]

start = time.time()
radix_sort_lsd(perf_data[:])
print(f"基数排序(LSD, base=10): {time.time() - start:.4f}s")

start = time.time()
radix_sort_lsd(perf_data[:], base=100)
print(f"基数排序(LSD, base=100): {time.time() - start:.4f}s")

start = time.time()
radix_sort_lsd(perf_data[:], base=1000)
print(f"基数排序(LSD, base=1000): {time.time() - start:.4f}s")

start = time.time()
sorted(perf_data[:])
print(f"内置sorted:              {time.time() - start:.4f}s")

典型输出:

复制代码
--- 性能对比 (n=10000, 范围0~99999) ---
基数排序(LSD, base=10): 0.0103s
基数排序(LSD, base=100): 0.0062s
基数排序(LSD, base=1000): 0.0045s
内置sorted:              0.0012s

结果分析

  • base=10:需要 5 轮(99999 是 5 位数),每轮 10 个桶
  • base=100:需要 3 轮,每轮 100 个桶 → 轮数减少 40%,速度提升 40%
  • base=1000:需要 2 轮,每轮 1000 个桶 → 最优平衡,速度最快
  • 更大基数:虽然只有 1~2 轮,但每轮的桶管理开销和空间增大,不再有优势

不同基数的理论分析

基数 轮数 d 每轮操作 总操作量 适用场景
10 5 n+10 5n+50 简单易懂,教学
100 3 n+100 3n+300 较优,Python 实用
1000 2 n+1000 2n+2000 Python 最优
65536 1~2 n+65536 ~2n+131072 C/C++(按双字节处理)

选型建议

场景 推荐算法 原因
大范围整数排序 基数排序 不退化,稳定线性
字符串排序 基数排序(MSD/LSD) 按字符逐位排序
小范围整数 计数排序 更简单直接
浮点数 桶排序 基数排序不适合浮点数
通用排序 TimSort / Introsort 适用性最广
需要稳定 + 大整数 基数排序 唯一稳定且线性的选择

六、工程实战

场景一:后缀数组构造

后缀数组是字符串算法的核心数据结构,在生物信息学、数据压缩等领域有广泛应用。一种经典的后缀数组构造方法就是基于基数排序的倍增法

python 复制代码
# 伪代码:后缀数组的倍增构造法
def build_suffix_array(s):
    """
    用基数排序 + 倍增法构造后缀数组。

    核心思路:
    1. 初始按单个字符排序
    2. 每轮倍增比较长度,用基数排序按"双关键字"排序
    3. 直到所有后缀的排名都唯一

    时间复杂度:O(n log n),其中每轮用基数排序 O(n)
    """
    n = len(s)
    # 初始:按单个字符排序
    rank = [ord(c) for c in s]
    sa = list(range(n))

    k = 1
    while k < n:
        # 按 (rank[i], rank[i+k]) 双关键字排序
        # 第一关键字 rank[i+k] 先用计数排序
        # 第二关键字 rank[i] 再用计数排序
        # 基数排序的稳定性保证正确
        sa = _radix_sort_double_key(sa, rank, k, n)
        # 更新 rank
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            new_rank[sa[i]] = new_rank[sa[i-1]]
            if _compare(s, sa[i], sa[i-1], k, n) != 0:
                new_rank[sa[i]] += 1
        rank = new_rank
        if rank[sa[-1]] == n - 1:
            break  # 所有排名唯一,完成
        k *= 2

    return sa

这个算法的核心是用基数排序按双关键字排序------先按第二关键字排序,再按第一关键字排序,利用稳定性得到正确的双关键字排序结果。这正是基数排序的典型应用。

场景二:大整数排序

当整数范围非常大(如 64 位整数)时,比较排序需要 O(n log n),而基数排序可以用较大的基数做到接近线性:

python 复制代码
# 排序 100 万个 64 位随机整数
import random
data = [random.randint(0, 2**63 - 1) for _ in range(1000000)]

# 基数排序 base=2^16(按双字节处理)
# 64位整数 = 4 个 16 位段 → 只需 4 轮
sorted_data = radix_sort_lsd(data, base=65536)

在 C/C++ 中,按字节(base=256)或双字节(base=65536)处理 64 位整数只需要 4~8 轮,每轮都是 O(n) 的线性扫描。对于大规模整数排序,基数排序在工程上有实际优势。

场景三:IP 地址排序

IP 地址(如 192.168.1.100)天然可以看作 4 个"位"的基数排序:

python 复制代码
def sort_ip_addresses(ips):
    """用基数排序对 IP 地址排序。"""
    # 将 IP 转换为 4 字节整数
    def ip_to_int(ip):
        parts = ip.split('.')
        return (int(parts[0]) << 24) + (int(parts[1]) << 16) + \
               (int(parts[2]) << 8) + int(parts[3])

    nums = [ip_to_int(ip) for ip in ips]
    sorted_nums = radix_sort_lsd(nums, base=256)  # 4 轮

    def int_to_ip(n):
        return f"{(n >> 24) & 255}.{(n >> 16) & 255}.{(n >> 8) & 255}.{n & 255}"

    return [int_to_ip(n) for n in sorted_nums]

IP 地址本质是一个 32 位无符号整数,用基数排序只需 4 轮(base=256),每轮 256 个桶,非常适合。


七、常见误区与面试题

高频面试题

Q1:基数排序的时间复杂度是多少?d 和 base 分别是什么?

基数排序的时间复杂度是 O(d · (n + base)),其中:

  • d 是数据的最大位数(如 5 位十进制数,d = 5)
  • base 是基数(如十进制 base=10,每个位有 0~9 共 10 种值)
  • n 是元素个数

当 base 是常数时,d ≈ log_base(max_val),总时间 O(n log n)。当 base = n 时,d ≈ 常数,总时间 O(n)。所以基数排序在基数选择合理时可以做到线性时间。

Q2:为什么 LSD 基数排序必须从最低位开始?能不能从最高位开始?

LSD 必须 从最低位开始。原因是:如果从高位开始且不做递归处理,高位排序会被低位排序打乱

从低位开始时,每轮排序利用了稳定性------高位相同的元素,其相对顺序由低位排序结果决定。这恰好是我们想要的:高位决定大区间顺序,低位决定小区间内顺序。

如果从高位开始,需要像 MSD 那样递归处理每个桶的低位------因为高位分桶后,桶内元素的低位尚未排序,需要递归。

Q3:基数排序是稳定的吗?为什么?

稳定排序 。稳定性的来源是每轮计数排序的稳定性

稳定性的传递链:第 1 轮按个位排序,相同个位的元素保持原始顺序;第 2 轮按十位排序,相同十位的元素保持第 1 轮的顺序;...;第 d 轮按最高位排序,相同最高位的元素保持第 d-1 轮的顺序。

如果任意一轮的排序不稳定,低位排序的相对顺序就会丢失,整个排序就不正确了。

Q4:基数排序和计数排序有什么关系?

基数排序是多轮计数排序的串联。每一轮按一个"位"做计数排序,多轮组合起来就完成了对整个数值的排序。

计数排序是基数排序的"原子操作"------没有计数排序(或任何稳定的按位排序算法),基数排序就无法工作。反过来说,计数排序也可以看作基数排序的退化情况:当数据只有 1 位(即取值范围等于基数)时,基数排序退化为单轮计数排序。

Q5:基数排序能排浮点数吗?

理论上可以,但需要特殊处理。浮点数的 IEEE 754 表示有一个特性:正浮点数的二进制表示按字典序排列就是升序排列(因为指数在前,尾数在后,且都是偏移编码)。

所以可以将浮点数的二进制字节直接作为"位"来排序。但负浮点数需要额外处理(符号位取反等),实现较复杂。实际中,浮点数通常用桶排序而非基数排序。

常见实现错误

错误 说明 修正
LSD 从高位开始 低位排序结果被高位覆盖 必须从最低位开始
计数排序不稳定 低位排序结果丢失 用从后往前遍历的稳定版本
轮数不够 高位没处理完,排序不完整 while max_val // exp > 0
位提取错误 val % exp 而非 (val // exp) % base 先除后取余
负数直接排序 符号位导致排序错误 分离正负数分别处理
字符串不等长 补位字符用错(如补空格) 补 '\0'(ASCII 最小)

八、总结

核心要点

  1. 按位分解------将大整数排序分解为多轮小范围计数排序
  2. 稳定传递------每轮计数排序的稳定性保证低位结果不被打乱
  3. LSD 从低到高------迭代实现,简单且缓存友好
  4. MSD 从高到低------递归实现,支持提前终止,适合变长数据
  5. 不依赖分布------与桶排序不同,基数排序不退化

适用边界与限制

维度 适用条件 不适用条件
数据类型 整数、字符串(可按位分解) 浮点数(需特殊处理)
位数 位数有限(d 较小) 位数极多(如超长字符串)
空间 能接受 O(n + base) 额外空间 空间极度受限
稳定性 需要稳定排序 不需要稳定性(可用快排)
数据范围 范围大但位数少 范围小(用计数排序更简单)

设计哲学

基数排序的设计哲学是:将大问题分解为多个小问题的串联,用稳定性保证串联的正确性

排序一个大整数(范围 0~2³²)是一个"大问题"------直接用计数排序需要 4GB 空间。但如果我们把这个大问题分解为"按字节排序"的小问题------每个字节只有 256 种值,计数排序只需 256 个桶------4 轮小问题的串联就完成了大问题的求解。

这种"分而治之"的思路与归并排序的分治不同。归并排序的"分治"是把数组分成两半,各自排序后合并------子问题是"排序半个数组"。基数排序的"分治"是把排序条件分解------按第 1 位排序 + 按第 2 位排序 + ... + 按第 d 位排序------子问题是"按某一个位排序"。

稳定性的角色:稳定性是连接这些子问题的"粘合剂"。每一轮排序的输出是下一轮排序的输入,稳定性保证了前面的工作不会被后面的工作"推翻"。没有稳定性,串联就失去了意义------每一轮都在破坏前面轮次的结果。

这个思想在计算机科学中非常通用:当一个大问题可以分解为多个有序的子问题,且子问题之间的关系可以通过某种"不变量"(如稳定性)维持时,就能用串联的方式求解。基数排序是这一思想的经典范例。


📌 专栏导航算法

⬅️ 上一篇桶排序 (Bucket Sort) ➡️ 下一篇:(待更新)

如果这篇文章对你有帮助,欢迎 点赞、收藏、关注,支持专栏持续更新!

相关推荐
lch2011_yb20 分钟前
CSP-J 2022 上升点列 题解
算法·深度优先
今天AI了吗25 分钟前
从“金鱼脑”到“大象记忆”:AI Agent 短期记忆与长期记忆的存储与检索全解
数据库·人工智能·python·sql·rust
lvts_cs30 分钟前
化工产业规划对城市发展的具体指导作用
算法·动态规划
whcyhhh42 分钟前
头歌实践教学平台:数据科学与大数据技术导论(十八4)
大数据·开发语言·python
编码者卢布42 分钟前
【Azure Developer】通过 API 获取 Azure VM 信息实践指南 Part 2(Azure China)
python·flask·azure
65岁退休Coder1 小时前
LangGraph v1.2.9 核心概念 & 流程控制
后端·python·langchain
AI服务老曹1 小时前
水位识别算法接入AI视频分析平台全流程与误报优化指南(附接口字段与排查表)
人工智能·算法·音视频
IPdodo_1 小时前
2026年AI 数据采集代理 IP 选型:成功率、并发、轮换与成本评估
前端·网络·人工智能·chrome·python·http·网络调试
江畔柳前堤1 小时前
消息队列:从直觉到精通的完整认知地图
开发语言·人工智能·算法·机器学习·ruby on rails·scala