cv.cm

编码 · 算法

基数排序详解:原理、代码与时间复杂度

阅读约 6 分钟

简短回答

基数排序是一种非比较排序算法,每次按一位数字(或一个字符)对整数或定长字符串排序。常见的最低位优先(LSD)版本先按最后一位排序,再按倒数第二位,直到最高位,每一趟都使用计数排序之类的稳定排序。位数为 d、基数为 b 时,它的时间是 O(d · (n + b)),额外空间是 O(n + b)。由于每一趟都是稳定的,最终结果完全有序,并且是稳定的。

打开行排序

基数排序解决什么问题

计数排序是线性的,但需要一个与整个键范围一样大的计数数组,这对 32 位或 64 位整数来说不可能。基数排序把每个键拆成小的数位,每位运行一次计数排序,因此每一趟的计数数组只需 b 项(十进制位是 10,字节是 256)。

它适用于任何能拆成固定长度的小符号序列的键:非负整数、写成 YYYYMMDD 的日期、邮政编码这类定长编码、IP 地址以及定宽字符串。

示例:十进制的 LSD 基数排序

对 [170, 45, 75, 90, 802, 24, 2, 66] 排序。最大的数有 3 位,所以共 3 趟。每一趟都是按一位数字做的稳定排序:数位相同的元素保持上一趟的顺序。

趟数排序依据结果
开始[170, 45, 75, 90, 802, 24, 2, 66]
1个位[170, 90, 802, 2, 24, 45, 75, 66]
2十位[802, 2, 24, 45, 66, 170, 75, 90]
3百位[2, 24, 45, 66, 75, 90, 170, 802]

第 2 趟之后,数字已按末两位有序。在第 3 趟中,2、24、45、66、75 和 90 的百位都是 0,稳定性使它们保持第 2 趟产生的顺序。这就是每一趟都必须稳定的原因。

Python 实现

def counting_pass(nums, exp, base):
    """Stable counting sort of nums by the digit at exp."""
    count = [0] * base
    for x in nums:
        count[(x // exp) % base] += 1
    for d in range(1, base):
        count[d] += count[d - 1]
    out = [0] * len(nums)
    for x in reversed(nums):
        d = (x // exp) % base
        count[d] -= 1
        out[count[d]] = x
    return out

def radix_sort(nums, base=10):
    """LSD radix sort for non-negative integers."""
    if not nums:
        return []
    exp, biggest = 1, max(nums)
    while biggest // exp > 0:
        nums = counting_pass(nums, exp, base)
        exp *= base
    return nums

print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]
print(radix_sort([3, 0, 1, 0], base=256))   # [0, 0, 1, 3]

以 256 为基数,意味着每一趟查看一个字节,所以 32 位整数只需 4 趟。要包含负数,可以把负数和非负数分开排序再拼接,或者把所有键偏移最小值。对于定长字符串,把位置 i 处的字符编码当作数位,从最后一个位置向第一个位置逐位处理。

时间与空间复杂度

指标值原因
时间(任何情况)O(d · (n + b))d 趟,每趟是对 n 个键、b 种数位值做的计数排序
额外空间O(n + b)每趟一个输出缓冲区和一个计数数组
32 位键的趟数基数 256 时为 432 位 ÷ 每位 8 比特
比较次数0数位被用作数组下标

当 d 是常数(定宽键)且 b 取接近 n 或固定值(如 256)时,时间是 O(n)。对 n 个互不相同的键,d 至少为 log_b n,所以对任意大小的键,基数排序并不能突破 O(n log n);它之所以快,是因为每一步都只是简单的数组操作。

基数排序与其他排序算法的比较

算法最好平均最坏额外空间稳定
基数排序(LSD)O(d · (n + b))O(d · (n + b))O(d · (n + b))O(n + b)是
计数排序O(n + k)O(n + k)O(n + k)O(n + k)是
归并排序O(n log n)O(n log n)O(n log n)O(n)是
快速排序O(n log n)O(n log n)O(n²)O(log n)否
堆排序O(n log n)O(n log n)O(n log n)O(1)否
Timsort(Python 的 sorted)O(n)O(n log n)O(n log n)O(n)是

表中 n 是键的个数,k 是计数排序的键范围,d 是位数,b 是基数。只要每一位的排序趟都是稳定的,LSD 基数排序就是稳定的。它要求键具有固定的结构,而归并排序和快速排序这类比较排序适用于任何顺序,包括自定义的比较函数。

LSD 与 MSD,以及常见错误

LSD(最低位优先)MSD(最高位优先)
各趟顺序从最后一位到第一位从第一位到最后一位,对每个桶递归处理
最适合定长整数和字符串变长字符串,遇到唯一前缀可提前结束
实现简单的循环递归;对小桶通常切换到插入排序
稳定是,前提是每趟稳定是,前提是桶内分配稳定

何时使用基数排序

基数排序很适合对大量定宽整数或键排序,例如 32 位 ID、时间戳、IPv4 地址或定长字符串,尤其是在 GPU 和高性能库中,它规整的内存访问方式能带来收益。它被用于后缀数组构造,以及某些数据库和图形负载。对于普通的 Python 列表,sorted() 在实践中几乎总是更快,因为它运行的是优化过的 C 代码。面试中,被问到如何用线性时间排序时会谈到基数排序,例如「最大间距」(Maximum Gap)这道题。

常见问题

基数排序稳定吗?

当每一位的排序趟都使用计数排序这样的稳定排序时,LSD 基数排序是稳定的。算法本身实际上正是依赖这种稳定性才能保证正确。

基数排序的时间复杂度是多少?

O(d · (n + b)),其中 d 是位数,n 是键的个数,b 是基数。对定宽键来说,d 和 b 是常数,所以它对 n 是线性的。

基数排序比快速排序快吗?

对大量定宽整数的数组,可能更快,因为它不做比较,内存访问模式也很规整。对通用数据、小数组或自定义顺序,快速排序或内置排序通常更好。

LSD 与 MSD 基数排序有什么区别?

LSD 从最低位到最高位排序,每位只需一趟简单的处理。MSD 从最高位开始,对每个桶递归排序,适合变长字符串。

基数排序能给字符串排序吗?

可以。把每个字符当作一位数字。LSD 可直接用于等长字符串;MSD 基数排序则能处理长度不同的字符串。

全部编码教程