Skip to content

Commit 4030114

Browse files
chore: new article written
1 parent beb9bd8 commit 4030114

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
title: "深入理解并实现基本的基数排序(Radix Sort)算法"
3+
author: "黄梓淳"
4+
date: "Sep 15, 2025"
5+
description: "深入理解基数排序算法原理"
6+
latex: true
7+
pdf: true
8+
---
9+
10+
11+
排序算法是计算机科学中的基础主题,传统上我们依赖于比较操作,如快速排序或归并排序,它们通过元素间的比较来确定顺序。但有一个问题:排序一定要通过两两比较吗?答案是否定的。基数排序(Radix Sort)提供了一种全新的视角,它是一种非比较型整数排序算法,基于键值的各位数字或字符进行逐位处理。这种方法的核心思想是稳定地按位排序,从而避免了许多比较操作带来的开销。
12+
13+
基数排序的核心价值在于其线性时间复杂度,通常表示为 $O(n \timesk)$,其中 $n$ 是元素个数,$k$ 是最大位数。这使得它在处理大规模固定长度数据时表现出色,例如排序手机号码、学号或 IP 地址。在本文中,我将带领您深入理解基数排序的原理,亲手实现它,并分析其性能与适用边界。通过这篇文章,您将不仅学会如何编码,还能 grasp 其背后的思想。
14+
15+
## 算法核心原理剖析
16+
17+
基数排序的基本思想是逐位处理数字,从最低位(LSD,Least Significant Digit)或最高位(MSD,Most Significant Digit)开始排序。一个经典的类比是扑克牌排序:假设您有一副牌,您可能先按花色排序,再按数字排序,但为了保持顺序,需要确保排序是稳定的。稳定性意味着相同键值的元素在排序后保持原有相对顺序,这对基数排序至关重要,因为高位排序时不能打乱低位已排好的顺序。
18+
19+
在 LSD 方法中,我们从最低位(如个位)开始,逐步向高位推进。例如,考虑数组 `[170, 45, 75, 90, 802, 24, 2, 66]`。首先,我们找到最大数字的位数(这里是 3,因为 802 有三位)。然后,进行三轮排序:第一轮按个位排序,第二轮按十位,第三轮按百位。每一轮使用一个稳定的排序算法(通常是计数排序)来处理当前位。可视化过程如下:初始数组为 `[170, 45, 75, 90, 802, 24, 2, 66]`;按个位排序后,顺序变为 `[170, 90, 802, 2, 24, 45, 75, 66]`;按十位排序后,变为 `[802, 2, 24, 45, 66, 170, 75, 90]`;最后按百位排序,得到最终有序数组 `[2, 24, 45, 66, 75, 90, 170, 802]`。这个过程展示了如何通过逐位稳定排序达到整体有序。
20+
21+
## 实现细节与代码解析
22+
23+
在实现基数排序时,我们选择计数排序作为辅助算法,因为它具有稳定性和线性时间复杂度,完美契合基数排序的需求。计数排序的核心是统计每个数字出现的次数,并通过累积计数来确定元素位置。下面,我将使用 Python 语言逐步实现 LSD 基数排序,并对代码进行详细解读。
24+
25+
首先,我们需要两个辅助函数:一个用于获取数组中的最大数字的位数,另一个用于获取数字在特定位上的数字。函数 `getMaxDigits` 遍历数组,找到最大数字并计算其位数。这通过将数字转换为字符串并取长度来实现,或者通过数学运算如连续除以 10。函数 `getDigit` 则提取数字在指定位上的数字,例如对于数字 123 和位索引 1(从右向左,0-based),它返回十位数字 2。
26+
27+
核心函数 `radixSort` 的实现步骤如下:计算最大位数 k,然后循环 k 次(从最低位到最高位)。在每一轮循环中,初始化一个大小为 10 的计数数组(因为十进制数字范围是 0-9)。接着,进行计数阶段:遍历数组,统计当前位上每个数字出现的次数。然后,将计数数组转换为累积计数数组,这有助于确定每个数字的最终位置。最后,重构数组:从后向前遍历原数组,根据当前位数字和计数数组,将元素放入临时输出数组的正确位置。从后向前遍历是为了保持稳定性,确保相同数字的元素顺序不变。完成后,将输出数组复制回原数组。
28+
29+
以下是 Python 代码实现:
30+
31+
```python
32+
def getMaxDigits(arr):
33+
# 找到数组中的最大数字
34+
max_val = max(arr)
35+
# 计算最大数字的位数:通过转换为字符串取长度
36+
return len(str(max_val))
37+
38+
def getDigit(num, digit):
39+
# 获取数字在指定位上的数字,digit 从 0 开始(0 表示个位)
40+
# 例如,getDigit(123, 1) 返回 2(十位)
41+
return (num // (10 ** digit)) % 10
42+
43+
def radixSort(arr):
44+
# 获取最大位数
45+
k = getMaxDigits(arr)
46+
# 临时输出数组
47+
output = [0] * len(arr)
48+
# 进行 k 轮排序
49+
for digit in range(k):
50+
# 初始化计数数组,大小为 10(0-9)
51+
count = [0] * 10
52+
# 计数阶段:统计当前位上每个数字的出现次数
53+
for num in arr:
54+
d = getDigit(num, digit)
55+
count[d] += 1
56+
# 累加计数:将计数数组转换为累积计数
57+
for i in range(1, 10):
58+
count[i] += count[i-1]
59+
# 重构数组:从后向前遍历原数组,以保持稳定性
60+
for i in range(len(arr)-1, -1, -1):
61+
num = arr[i]
62+
d = getDigit(num, digit)
63+
output[count[d] - 1] = num
64+
count[d] -= 1
65+
# 将输出数组复制回原数组
66+
arr = output[:]
67+
return arr
68+
```
69+
70+
代码解读:在 `getMaxDigits` 函数中,我们使用 `max` 函数找到最大值,然后通过 `len(str(max_val))` 计算位数,这是一种简单直接的方法。在 `getDigit` 函数中,我们使用整数除法和模运算来提取特定位上的数字,例如 `(num // (10 ** digit)) % 10` 计算数字在 digit 位上的值。在 `radixSort` 函数中,外层循环运行 k 次,对应每位排序。计数数组 `count` 初始化为全零,然后遍历数组统计数字出现次数。累加计数步骤将 `count` 数组转换为每个数字的结束索引加一。重构数组时,从后向前遍历原数组,确保稳定性:将元素放入输出数组的指定位置,并递减计数。最后,复制输出数组回原数组,完成排序。这个实现的时间复杂度为 $O(n\timesk)$,空间复杂度为 $O(n + 10)$,由于 10 是常数,通常简化为 $O(n)$。
71+
72+
## 进阶讨论与变体
73+
74+
基数排序的复杂度分析显示,时间复杂度为 $O(n\timesk)$,其中 n 是元素个数,k 是最大位数。由于 k 通常相对较小(例如,对于 32 位整数,k 最大为 10),这可以被视为线性时间复杂度,优于许多比较排序算法如快速排序的 $O(n \log{n})$。空间复杂度为 $O(n + b)$,b 是基数大小(这里 b=10),主要开销来自输出数组和计数数组。
75+
76+
LSD 和 MSD 是基数排序的两种变体。LSD 从最低位开始排序,实现简单,适用于位数较少的数,但必须完成所有位的排序。MSD 从最高位开始,类似递归的桶排序,可能不需要比较所有位(如果高位已能区分大小),但实现更复杂,需要处理递归开销和空桶,适用于字符串排序或字典序场景。
77+
78+
处理负数时,基数排序需要额外步骤。常见方法是将数组分割成负数和正数两部分。对负数部分,取绝对值后使用基数排序,然后反转顺序(因为负数取绝对值后排序顺序相反)。对正数部分直接排序,最后合并两部分。另一种方法是偏移法:将所有数加上一个最小值偏移量,使其变为非负数,排序后再减回去。例如,如果数组中有负数,先找到最小值 min_val,然后将每个元素加上 abs(min_val),排序后再减去 abs(min_val)。
79+
80+
81+
基数排序的优点包括线性时间复杂度,高效处理大规模固定长度数据,如整数或字符串。缺点是非原地排序,需要额外空间;对数据类型有限制(只适用于可分解为位的类型);当 k 很大时(数字非常长),效率可能下降。
82+
83+
典型应用场景包括数据库中对整数键的排序、计算机图形学中的算法(如深度排序)、以及后缀数组的构造。在这些领域,基数排序的线性性能优势明显。
84+
85+
结束语:基数排序是一种独特而高效的算法,它突破了传统比较排序的局限。通过理解其原理和实现,您可以更好地应用它到实际问题中。我鼓励您动手实现一遍代码,以加深印象。
86+
87+
## 互动与延伸
88+
89+
思考题:如何修改代码来排序字符串数组?例如,对单词列表按字典序排序,这可以通过将每个字符视为一位,使用类似方法处理。如果数字的进制不是十进制,比如二进制或十六进制,算法需要调整基数大小(b),例如二进制时 b=2,计数数组大小相应改变。
90+
91+
相关资源:您可以访问可视化排序网站如 VisuAlgo,观看基数排序的动态演示。对于更深入的讨论,推荐阅读关于 MSD 基数排序的学术文章,以探索其递归实现和优化。

0 commit comments

Comments
 (0)