|
| 1 | +--- |
| 2 | +title: "深入理解并实现基本的基数排序(Radix Sort)算法" |
| 3 | +author: "杨子凡" |
| 4 | +date: "Nov 13, 2025" |
| 5 | +description: "基数排序原理与实现详解" |
| 6 | +latex: true |
| 7 | +pdf: true |
| 8 | +--- |
| 9 | + |
| 10 | +在数据处理领域,排序是一项基础而关键的操作。当我们面对海量数据时,例如为成千上万的手机号码排序,传统的比较排序算法如快速排序或归并排序虽然高效,但它们的时间复杂度存在一个理论下限 $O(n \log{n})$。这自然引出一个问题:是否存在一种不通过比较就能实现排序的算法?答案是肯定的,基数排序正是这样一种非比较型的整数排序算法。它的核心思想是将整数按位数「切割」,逐位进行排序,在特定条件下时间复杂度甚至可以达到 $O(n)$。本文旨在带领读者彻底理解基数排序的原理,掌握其手动实现方法,并清晰认识其优缺点与适用场景。 |
| 11 | + |
| 12 | +## 核心思想与原理 |
| 13 | + |
| 14 | +基数排序的命名来源于「基数」这一概念。基数指的是进制的基数,例如在十进制中,基数为10,这意味着我们需要10个「桶」来分别存放数字0到9。关键码是排序所依据的属性,对于整数排序而言,关键码就是数字本身。稳定性是排序算法的一个重要特性,它表示如果两个元素在排序前相等,那么排序后它们的相对顺序保持不变。在基数排序中,稳定性至关重要,因为后序位的排序不能打乱前序位已经排好的顺序。 |
| 15 | + |
| 16 | +基数排序有两种主要实现方式:最低位优先(LSD)和最高位优先(MSD)。LSD 从最低位(如个位)开始排序,依次向最高位进行,实现简单直观,是本文主要讲解的方式。MSD 则从最高位开始,然后递归地对每个桶内的数据进行下一位排序,更像一种分治策略,但实现稍显复杂。通过对比,LSD 因其易于理解和实现,常被用作入门教学的首选。 |
| 17 | + |
| 18 | +## 逐步拆解 LSD 基数排序过程 |
| 19 | + |
| 20 | +让我们通过一个具体示例来逐步拆解 LSD 基数排序的过程。假设我们有数组 `[170, 45, 75, 90, 2, 802, 2, 66]`。首先,我们需要找到数组中的最大数字,以确定最大位数。这里最大数字是802,有3位,因此我们需要进行3轮排序。 |
| 21 | + |
| 22 | +第一轮从最低位(个位)开始。我们创建10个桶,对应数字0到9。然后遍历数组,根据每个数字的个位数字将其放入对应的桶中。例如,170的个位是0,放入桶0;45的个位是5,放入桶5;以此类推。分配完成后,我们按桶号0到9的顺序依次收集元素放回数组。此时,数组按个位数字排序,结果为 `[170, 90, 2, 802, 2, 45, 75, 66]`。 |
| 23 | + |
| 24 | +第二轮处理十位数字。同样,创建10个桶,根据十位数字分配元素。例如,170的十位是7,放入桶7;90的十位是9,放入桶9。由于排序是稳定的,个位相同的数字在十位排序时会保持相对顺序。收集后,数组按十位和个位排序,结果为 `[2, 802, 2, 45, 66, 170, 75, 90]`。 |
| 25 | + |
| 26 | +第三轮处理百位数字,过程相同。完成后,整个数组有序,最终结果为 `[2, 2, 45, 66, 75, 90, 170, 802]`。这个示例清晰地展示了 LSD 基数排序的逐位排序过程,强调了稳定性在保持顺序中的作用。 |
| 27 | + |
| 28 | +## 动手实现基数排序 |
| 29 | + |
| 30 | +以下是用 Python 实现 LSD 基数排序的代码。我们将逐步解释关键部分,确保读者能够理解每一行代码的作用。 |
| 31 | + |
| 32 | +```python |
| 33 | +def radix_sort(arr): |
| 34 | + # 步骤一:寻找最大值与最大位数 |
| 35 | + max_num = max(arr) |
| 36 | + max_digit = len(str(max_num)) # 通过转换为字符串获取位数 |
| 37 | + |
| 38 | + # 步骤二:核心排序循环 |
| 39 | + for digit in range(max_digit): |
| 40 | + # 创建 10 个桶 |
| 41 | + buckets = [[] for _ in range(10)] |
| 42 | + |
| 43 | + # 分配过程:根据当前位数字将元素放入对应桶 |
| 44 | + for num in arr: |
| 45 | + current_digit = (num // (10 ** digit)) % 10 |
| 46 | + buckets[current_digit].append(num) |
| 47 | + |
| 48 | + # 收集过程:按顺序将桶中元素放回数组 |
| 49 | + arr = [] |
| 50 | + for bucket in buckets: |
| 51 | + arr.extend(bucket) |
| 52 | + |
| 53 | + return arr |
| 54 | +``` |
| 55 | + |
| 56 | +现在,让我们详细解读这段代码。在步骤一中,我们使用 `max(arr)` 找到数组中的最大值 `max_num`,然后通过 `len(str(max_num))` 计算其位数 `max_digit`。这里,将数字转换为字符串后取长度是一种简单直观的方法,用于确定排序的轮数。 |
| 57 | + |
| 58 | +在核心循环中,`digit` 从0到 `max_digit-1` 迭代,表示当前处理的位数(0表示个位,1表示十位,以此类推)。对于每一轮,我们使用列表推导式创建10个空桶 `buckets`。 |
| 59 | + |
| 60 | +分配过程中,对于每个数字 `num`,我们计算当前位的数字。表达式 `(num // (10 ** digit)) % 10` 是关键:当 `digit=0`(个位)时,`10 ** 0` 等于1,`num // 1` 仍是 `num`,然后 `% 10` 得到个位数字;当 `digit=1`(十位)时,`10 ** 1` 等于10,`num // 10` 去掉个位,然后 `% 10` 得到十位数字。这样,我们就能准确提取指定位的数字值。 |
| 61 | + |
| 62 | +收集过程时,我们初始化一个新数组 `arr`,然后按桶号0到9的顺序,使用 `extend` 方法将每个桶中的元素依次添加回数组。由于 `extend` 保持元素顺序,且桶是按数字顺序创建的,这确保了排序的稳定性。最终,函数返回排序后的数组。 |
| 63 | + |
| 64 | +## 深入分析与探讨 |
| 65 | + |
| 66 | +基数排序的性能分析是理解其优势的关键。设待排序元素个数为 $n$,最大位数为 $k$,基数为 $r$(在十进制中 $r=10$)。每一轮分配需要遍历所有元素,时间复杂度为 $O(n)$,收集同样需要 $O(n)$ 时间。由于有 $k$ 轮,总时间复杂度为 $O(k\times{n})$。当 $k$ 远小于 $n$ 时,性能接近线性,表现出高效性。 |
| 67 | + |
| 68 | +空间复杂度方面,我们需要额外的 $O(n + r)$ 空间来存储桶和元素。这是一种典型的以空间换时间策略,在内存充足的情况下值得采用。 |
| 69 | + |
| 70 | +基数排序的优点包括时间复杂度可能达到 $O(n)$,且是稳定排序。缺点是非原地排序,需要额外空间,且适用范围有限,通常只适用于整数或可表示为整数的类型。如果最大位数 $k$ 很大,效率会显著下降。 |
| 71 | + |
| 72 | +与其他排序算法相比,基数排序在特定场景下优势明显。例如,与快速排序相比,基数排序稳定且对数据分布不敏感;与计数排序相比,基数排序通过分治按位处理,避免了数据范围大时计数排序的空间消耗问题。 |
| 73 | + |
| 74 | +## 扩展与变种 |
| 75 | + |
| 76 | +标准 LSD 基数排序无法直接处理负数。一个常见的解决方案是将数组拆分为正数和负数两部分。对负数部分取绝对值进行排序,然后反转顺序(因为负数绝对值越大,实际值越小),再与正数部分合并。这样可以扩展算法的适用性。 |
| 77 | + |
| 78 | +MSD 基数排序从最高位开始,递归排序,适用于某些场景,如字符串排序,但实现更复杂。对于字符串,可以按字符的 ASCII 码逐位排序,原理类似整数排序。 |
| 79 | + |
| 80 | +基数排序也可以应用于其他数据类型,如日期,只要能将它们转换为整数序列。例如,日期可以表示为年月日的数字组合,然后按位排序。 |
| 81 | + |
| 82 | + |
| 83 | +本文详细介绍了基数排序的核心思想、LSD 实现步骤、性能分析和扩展应用。基数排序作为一种非比较排序算法,在特定条件下展现出高效性,尤其适用于整数排序场景。通过手动实现代码,读者可以更深入地理解其工作原理。鼓励读者在实践中探索基数排序的更多应用,例如在大数据处理或数据库索引中。 |
| 84 | + |
| 85 | +## 附录与思考题 |
| 86 | + |
| 87 | +在附录中,我们提出几个思考题供读者进一步探索。首先,考虑如何使用队列数据结构来优化桶的实现,使得收集过程更自然。其次,尝试修改代码以处理包含负数的数组。最后,如果待排序数字的范围已知且较小,可以探索用计数排序替代每轮的桶排序,以优化性能。这些练习有助于加深对基数排序的理解和应用。 |
0 commit comments