当前目录
← Back

Radix Sort 基数排序


Radix Sort 基数排序

核心结论

基数排序将一个关键字拆分成若干个“位”,逐位进行排序。

本文主要讨论 LSD Radix Sort(最低有效位优先基数排序):从最低位到最高位依次处理,并且每一趟必须使用稳定排序。

若共有 nn 个元素、每个关键字有 dd 位、每一位有 RR 种可能取值,并使用计数排序处理每一位,则:

T(n)=Θ(d(n+R))T(n)=\Theta\bigl(d(n+R)\bigr)

1. 问题定义

给定一个包含 nn 个非负整数的数组:

A=[A[0],A[1],…,A[n−1]]A=[A[0],A[1],\dots,A[n-1]]

将每个整数视为 RR 进制下由 dd 个数字组成的关键字,要求将数组按数值非递减顺序排列。

不足 dd 位的整数在高位补 00。例如,在十进制三位数排序中:

7≡007,42≡0427\equiv 007,\qquad 42\equiv 042

第 ii 位数字采用 0-based 编号,其中 i=0i=0 表示最低位:

digit⁡(x,i,R)=⌊xRi⌋ mod R\operatorname{digit}(x,i,R) = \left\lfloor\frac{x}{R^i}\right\rfloor\bmod R

例如,当 R=10R=10 时:

digit⁡(457,0,10)=7\operatorname{digit}(457,0,10)=7 digit⁡(457,1,10)=5\operatorname{digit}(457,1,10)=5 digit⁡(457,2,10)=4\operatorname{digit}(457,2,10)=4

2. 基本思想

基数排序不直接比较两个完整关键字的大小,而是把关键字拆成多个位,再逐位排序。

LSD 基数排序按照以下顺序处理:

  1. 先按最低位排序;
  2. 再按次低位排序;
  3. 重复该过程;
  4. 最后按最高位排序。

处理完第 ii 位后,数组已经按照低 i+1i+1 位组成的关键字有序。

为什么必须使用稳定排序

稳定排序保证:当两个元素当前位相同时,它们在上一趟排序中形成的相对次序不会改变。

因此,按第 ii 位排序时,已经按低 ii 位建立的顺序能够被保留下来。

例如,已知 329 和 355 的百位都为 3。在按百位排序时,稳定排序会保留它们此前根据十位和个位形成的顺序:

329 < 355

若辅助排序不稳定,这一顺序可能被颠倒,导致最终结果错误。

3. 具体过程

设数组中的最大元素在 RR 进制下最多有 dd 位。

3.1 算法步骤

  1. 确定最大关键字的位数 dd;
  2. 令当前位 i=0i=0,即最低位;
  3. 使用稳定排序,按照第 ii 位数字对整个数组排序;
  4. 令 i←i+1i\leftarrow i+1;
  5. 当 i=di=d 时结束,否则返回步骤 3。
graph TD
    A[输入数组 A] --> B[确定最大位数 d]
    B --> C[i = 0]
    C --> D[按第 i 位进行稳定排序]
    D --> E[i = i + 1]
    E --> F{i < d?}
    F -->|是| D
    F -->|否| G[输出有序数组]

3.2 每一趟排序的状态

在完成第 ii 趟排序后,数组按照每个元素的低 i+1i+1 位有序。

若使用十进制:

已处理的位当前排序依据
第 0 趟个位
第 1 趟十位和个位组成的两位数
第 2 趟百位、十位和个位组成的三位数
⋮\vdots⋮\vdots
第 d−1d-1 趟完整关键字

4. 伪代码

下面的伪代码统一使用 0-based 下标。

4.1 基数排序

RADIX-SORT(A, R)
    if LENGTH(A) <= 1
        return A

    max_value <- MAX(A)
    exp <- 1

    while floor(max_value / exp) > 0
        A <- STABLE-SORT-BY-DIGIT(A, exp, R)
        exp <- exp * R

    return A

其中:

  • R:基数,例如十进制时 R=10R=10;
  • exp:当前位的权值,依次为 1,R,R2,…1,R,R^2,\dots;
  • 当前位数字为:
⌊xexp⌋ mod R\left\lfloor\frac{x}{\text{exp}}\right\rfloor\bmod R

4.2 使用计数排序处理某一位

STABLE-SORT-BY-DIGIT(A, exp, R)
    n <- LENGTH(A)
    B <- new array of length n
    count <- new array of length R, initialized to 0

    for i <- 0 to n - 1
        digit <- floor(A[i] / exp) mod R
        count[digit] <- count[digit] + 1

    for digit <- 1 to R - 1
        count[digit] <- count[digit] + count[digit - 1]

    for i <- n - 1 downto 0
        digit <- floor(A[i] / exp) mod R
        B[count[digit] - 1] <- A[i]
        count[digit] <- count[digit] - 1

    return B

为什么从右向左扫描输入数组

构造输出数组时,从右向左扫描可以保证相同数字的元素保持原有相对顺序,因此这一趟计数排序是稳定的。

5. 正确性证明

5.1 循环不变式

在开始处理第 ii 位之前,数组已经按照低 ii 位有序。

等价地,对任意两个元素 xx 和 yy,它们的低 ii 位关键字为:

Li(x)=x mod RiL_i(x)=x\bmod R^i

若 Li(x)<Li(y)L_i(x)<L_i(y),则 xx 位于 yy 之前。

5.2 初始化

处理最低位之前,i=0i=0。

所有元素的低 00 位都可以视为空关键字,因此数组天然按照低 00 位有序,循环不变式成立。

5.3 维持

假设处理第 ii 位之前,数组已经按照低 ii 位有序。

现在使用稳定排序按照第 ii 位数字排序。

对于任意两个元素 xx 和 yy:

  • 若它们的第 ii 位不同,则当前排序会按照第 ii 位确定正确顺序;
  • 若它们的第 ii 位相同,则稳定性会保留它们根据低 ii 位形成的原有顺序。

因此,排序完成后,数组按照低 i+1i+1 位有序,循环不变式得到维持。

5.4 终止

处理完第 d−1d-1 位后,数组按照低 dd 位有序。

由于每个关键字最多只有 dd 位,低 dd 位就是完整关键字,因此数组整体有序。

正确性结论

只要每一趟使用稳定排序,LSD 基数排序就能得到完整关键字的正确非递减序列。

6. 时空间复杂度

设:

  • nn:元素个数;
  • dd:每个关键字的位数;
  • RR:每一位可能的取值数量,即基数;
  • 每一趟使用计数排序。

6.1 时间复杂度

计数排序处理一位的时间复杂度为:

Θ(n+R)\Theta(n+R)

一共处理 dd 位,因此总时间复杂度为:

Θ(d(n+R))\boxed{\Theta\bigl(d(n+R)\bigr)}
情况时间复杂度
一般情况Θ(d(n+R))\Theta(d(n+R))
R=O(n)R=O(n)Θ(dn)\Theta(dn)
d=O(1)d=O(1) 且 R=O(n)R=O(n)Θ(n)\Theta(n)

“线性时间”的前提

基数排序并非无条件为 Θ(n)\Theta(n)。

只有当位数 dd 为常数,且每一位的取值范围 RR 不超过 O(n)O(n) 时,才可以写成 Θ(n)\Theta(n)。

6.2 按位分组的复杂度

若每个关键字占 WW 个二进制位,每次取 rr 位作为一个数字,则:

  • 基数为 R=2rR=2^r;
  • 趟数为 ⌈W/r⌉\lceil W/r\rceil;
  • 每一趟计数排序耗时 Θ(n+2r)\Theta(n+2^r)。

因此:

T(n,W,r)=Θ(⌈Wr⌉(n+2r))T(n,W,r) = \Theta\left(\left\lceil\frac{W}{r}\right\rceil(n+2^r)\right)

增大 rr 会减少排序趟数,但会增大计数数组的规模 2r2^r,两者之间需要权衡。

6.3 空间复杂度

标准计数排序需要:

  • 输出数组:Θ(n)\Theta(n);
  • 计数数组:Θ(R)\Theta(R)。

因此辅助空间复杂度为:

Θ(n+R)\boxed{\Theta(n+R)}

标准实现不是原地排序。

7. 需要问题具有的性质

基数排序适用于满足以下条件的问题。

7.1 关键字可以分解为有限个位

每个关键字必须能够表示成:

(kd−1,kd−2,…,k1,k0)(k_{d-1},k_{d-2},\dots,k_1,k_0)

其中每一位 kik_i 都属于有限集合:

ki∈{0,1,…,R−1}k_i\in\{0,1,\dots,R-1\}

典型对象包括:

  • 非负整数;
  • 固定位宽二进制整数;
  • 定长字符串;
  • 能够映射为整数关键字的记录。

7.2 每一位的取值范围可控

若 RR 过大,计数数组会消耗大量时间和空间,使基数排序失去优势。

7.3 每一趟辅助排序必须稳定

这是 LSD 基数排序正确性的核心条件。

常用的稳定辅助排序包括:

7.4 所有关键字的位数需要统一

不同长度的整数可以在高位补 00。

不同长度的字符串需要额外定义:

  • 缺失字符的排序规则;
  • 字符表顺序;
  • 是否采用特殊哨兵字符。

7.5 排序顺序必须能由各位的字典序决定

对于非负整数,数值顺序等价于固定长度数字序列的字典序。

对任意两个 dd 位整数,最先出现差异的最高位决定两个整数的大小。

7.6 负数需要额外处理

上述基本算法默认输入为非负整数。

处理负数时,常见方法是:

  1. 将负数和非负数分开;
  2. 对负数的绝对值进行基数排序;
  3. 将负数部分逆序并恢复负号;
  4. 再拼接非负数部分。

警告

不能直接将带符号整数代入只支持非负数的数字提取公式,否则排序结果可能错误。

8. 典型例子

对数组进行十进制 LSD 基数排序:

A = [329, 457, 657, 839, 436, 720, 355]

所有元素均视为三位数,基数 R=10R=10。

8.1 第 1 趟:按个位稳定排序

元素个位
3299
4577
6577
8399
4366
7200
3555

排序结果:

[720, 355, 436, 457, 657, 329, 839]

8.2 第 2 趟:按十位稳定排序

[720, 329, 436, 839, 355, 457, 657]

此时数组已按照后两位有序:

20, 29, 36, 39, 55, 57, 57

8.3 第 3 趟:按百位稳定排序

[329, 355, 436, 457, 657, 720, 839]

所有位均已处理,得到最终有序数组。

9. Python 实现

from typing import List


def counting_sort_by_digit(values: List[int], exp: int, base: int) -> List[int]:
    """按照由 exp 指定的数位执行一次稳定计数排序。"""
    n = len(values)
    output = [0] * n
    count = [0] * base

    # 统计当前位上各数字出现的次数。
    for value in values:
        digit = (value // exp) % base
        count[digit] += 1

    # 转换为前缀和:count[d] 表示数字 <= d 的元素个数。
    for digit in range(1, base):
        count[digit] += count[digit - 1]

    # 从右向左放置元素,保证稳定性。
    for i in range(n - 1, -1, -1):
        value = values[i]
        digit = (value // exp) % base
        output[count[digit] - 1] = value
        count[digit] -= 1

    return output


def radix_sort(values: List[int], base: int = 10) -> List[int]:
    """使用 LSD 基数排序返回非负整数数组的有序副本。"""
    if base < 2:
        raise ValueError("base 必须不小于 2")

    if any(not isinstance(value, int) for value in values):
        raise TypeError("values 中的所有元素都必须是整数")

    if any(value < 0 for value in values):
        raise ValueError("该实现只支持非负整数")

    if len(values) <= 1:
        return values.copy()

    result = values.copy()
    max_value = max(result)
    exp = 1

    while max_value // exp > 0:
        result = counting_sort_by_digit(result, exp, base)
        exp *= base

    return result


if __name__ == "__main__":
    data = [329, 457, 657, 839, 436, 720, 355]
    result = radix_sort(data, base=10)

    print("原数组:", data)
    print("排序后:", result)

    assert result == sorted(data)

预期输出:

原数组: [329, 457, 657, 839, 436, 720, 355]
排序后: [329, 355, 436, 457, 657, 720, 839]

验证方式:

assert radix_sort(data) == sorted(data)

10. 与其他排序算法的比较

算法类型时间复杂度辅助空间稳定性
基数排序非比较排序Θ(d(n+R))\Theta(d(n+R))Θ(n+R)\Theta(n+R)取决于辅助排序
Counting Sort 计数排序非比较排序Θ(n+K)\Theta(n+K)Θ(n+K)\Theta(n+K)可以稳定
Merge Sort 归并排序比较排序Θ(nlog⁡n)\Theta(n\log n)Θ(n)\Theta(n)稳定
Quick Sort 快速排序比较排序平均 Θ(nlog⁡n)\Theta(n\log n)平均 Θ(log⁡n)\Theta(\log n)通常不稳定
Heap Sort 堆排序比较排序Θ(nlog⁡n)\Theta(n\log n)Θ(1)\Theta(1)不稳定

基数排序为什么能突破

Ω(nlog⁡n)\Omega(n\log n) Ω(nlog⁡n)\Omega(n\log n) 是比较排序模型的下界。

基数排序利用了关键字可以拆分为有限位这一额外结构,并不是仅通过元素间比较确定顺序,因此不受该下界直接约束。

11. 常见错误

错误 1:辅助排序不稳定

这会破坏上一趟已经建立的低位顺序。

错误 2:从最高位直接向最低位排序

对普通 LSD 算法框架而言,这样不能保证低位排序不会破坏高位顺序。MSD Radix Sort 需要使用递归分桶等另一套算法结构。

错误 3:忽略前导零

位数不同的数字必须视为高位补零,否则位的含义不统一。

错误 4:直接写成

O(n)O(n) 应先写出完整复杂度 Θ(d(n+R))\Theta(d(n+R)),再说明何种条件下可以化简为 Θ(n)\Theta(n)。

错误 5:基数选择过大

增大基数会减少趟数,但计数数组的时间和空间成本也会随之增大。

12. 总结

基数排序的核心可以概括为:

  1. 将关键字拆分成多个位;
  2. 从最低位到最高位依次排序;
  3. 每一趟必须使用稳定排序;
  4. 通过稳定性保留已经建立的低位顺序;
  5. 总时间复杂度为 Θ(d(n+R))\Theta(d(n+R));
  6. 当 d=O(1)d=O(1) 且 R=O(n)R=O(n) 时,可达到线性时间。

13. 相关笔记

14. 参考资料

  1. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to Algorithms, 3rd ed., Chapter 8.3: Radix Sort. MIT Press, 2009.
  2. Erik D. Demaine, Charles E. Leiserson. Introduction to Algorithms, Lecture 5: Sorting Lower Bounds and Linear-Time Sorting. MIT, 2005.
  3. Kepano. Obsidian Flavored Markdown Skill. obsidian-skills/skills/obsidian-markdown.
文章大纲
  1. Radix Sort 基数排序
  2. 1. 问题定义
  3. 2. 基本思想
  4. 3. 具体过程
  5. 3.1 算法步骤
  6. 3.2 每一趟排序的状态
  7. 4. 伪代码
  8. 4.1 基数排序
  9. 4.2 使用计数排序处理某一位
  10. 5. 正确性证明
  11. 5.1 循环不变式
  12. 5.2 初始化
  13. 5.3 维持
  14. 5.4 终止
  15. 6. 时空间复杂度
  16. 6.1 时间复杂度
  17. 6.2 按位分组的复杂度
  18. 6.3 空间复杂度
  19. 7. 需要问题具有的性质
  20. 7.1 关键字可以分解为有限个位
  21. 7.2 每一位的取值范围可控
  22. 7.3 每一趟辅助排序必须稳定
  23. 7.4 所有关键字的位数需要统一
  24. 7.5 排序顺序必须能由各位的字典序决定
  25. 7.6 负数需要额外处理
  26. 8. 典型例子
  27. 8.1 第 1 趟:按个位稳定排序
  28. 8.2 第 2 趟:按十位稳定排序
  29. 8.3 第 3 趟:按百位稳定排序
  30. 9. Python 实现
  31. 10. 与其他排序算法的比较
  32. 11. 常见错误
  33. 12. 总结
  34. 13. 相关笔记
  35. 14. 参考资料