排序:计算机科学的基础问题
排序是计算机科学中最基础也最实用的问题之一。从整理名单到组织数据,从版本号对齐到随机抽取,排序无处不在。但”排序”远不止”A 到 Z”这么简单——不同的排序模式适用于完全不同的场景。
本文将系统讲解 8 种排序模式及其适用场景。
配套工具:文本排序工具
若需将文本按相反顺序排列,可配合 文本倒序输出工具 实现逆序输出,常用于回文检测、栈结构演示、反向展示名单等场景。
一、8 种排序模式详解
1.1 字母升序 (A→Z) 与降序 (Z→A)
最经典的排序模式,按字符的 Unicode 码点逐字符比较。
升序: 降序:
Apple cherry
banana banana
cherry Apple
date date
注意:大写字母(A=65)的码点低于小写字母(a=97),因此在大小写敏感模式下,“Apple” 排在 “banana” 前面。关闭大小写敏感后统一转为小写比较,结果更符合阅读直觉。
1.2 数值升序与降序
将每行解析为数字后按数值大小排序,适合排序包含数字的行。
输入: 数值升序: 数值降序:
123 7 123
42 42 42
7 123 7
banana banana banana
非数字行统一排到末尾,保持原始相对顺序。parseFloat 会解析行首的数字部分,如 “3.14abc” 被解析为 3.14。
1.3 长度升序(短→长)与降序(长→短)
按行的字符长度排序,适合快速找出最长或最短的行。
长度升序: 长度降序:
hi elephant
cat banana
banana cherry
elephant cat
hi
长度相同时按字母顺序作为次级排序,保证稳定排序。
1.4 自然排序(Natural Sort)
自然排序是字母排序的”智能版”——它将字符串拆分为文本段与数字段交替比较,数字段按数值大小而非字符顺序比较。
字母排序: 自然排序:
item1 item1
item10 item2
item2 item3
item20 item10
item3 item20
自然排序的实现核心是模式拆分:用正则 /(\d+|\D+)/g 将字符串拆分为交替的数字段与非数字段,然后逐段比较——数字段按 BigInt 数值比较(支持超大整数),文本段按字符串比较。
// 自然排序的核心:拆分为交替的文本段与数字段
const parts = "item10".match(/(\d+|\D+)/g);
// → ["item", "10"]
// 文本段 "item" 按字符串比较
// 数字段 "10" 按数值 10 比较
使用 BigInt 而非 Number 是因为 JavaScript Number 受 2^53 限制,而版本号或 ID 可能超出此范围。
1.5 随机打乱(Shuffle)
将所有行随机重新排列,每次结果不同。使用 Fisher-Yates 洗牌算法 配合 crypto.getRandomValues 实现无偏差随机。
原始: 打乱后(每次不同):
Alice Charlie
Bob Alice
Charlie Bob
随机打乱的公平性是关键——详见下文算法分析。如需直接对列表执行随机打乱并导出结果,可用 列表随机打乱工具(基于 Fisher-Yates + crypto.getRandomValues 实现无偏差洗牌)。
二、自然排序的拆分比较算法
自然排序的核心难点在于”识别嵌入的数字”。考虑版本号 v1.2.10 与 v1.2.9:
- 字母排序:
v1.2.10<v1.2.9(因为 “1” < “9”) - 自然排序:
v1.2.10>v1.2.9(因为 10 > 9)
算法步骤
- 拆分:用正则
/(\\d+|\\D+)/g将字符串拆分为交替的数字段与非数字段 - 逐段比较:对齐两个字符串的段,逐段比较
- 数字段(
/^\\d+$/):用BigInt按数值比较 - 文本段:按字符串比较
- 数字段(
- 长度兜底:如果所有对齐段都相等,较短的段序列排在前面
边界处理
- 空字符串排在最前
- 纯数字行用 BigInt 比较,避免精度丢失
- 大小写敏感选项影响文本段的比较
三、Fisher-Yates 无偏差洗牌
随机打乱看似简单,但实现不当会引入偏差——某些排列出现的概率高于其他排列。
错误实现:sort + Math.random
// 错误:有偏差!
arr.sort(() => Math.random() - 0.5);
这种写法的问题是:sort 的比较函数不满足传递性(如果 A>B 且 B>C,不保证 A>C),导致排序结果分布不均匀。不同引擎的实现还会产生不同的偏差模式。
正确实现:Fisher-Yates + crypto
Fisher-Yates 算法是最经典的公平洗牌算法,时间复杂度 O(n):
for (let i = arr.length - 1; i > 0; i--) {
// 从 [0, i] 中随机选一个位置 j
const j = secureRandomInt(i + 1);
// 交换 arr[i] 和 arr[j]
[arr[i], arr[j]] = [arr[j], arr[i]];
}
公平性保证:每个位置被选中交换的概率完全相等,因此所有排列出现的概率都是 1/n!。
拒绝采样消除取模偏差
secureRandomInt(max) 使用 crypto.getRandomValues 生成 32 位随机整数后取模:
function secureRandomInt(max) {
const limit = Math.floor(0xffffffff / max) * max;
const buf = new Uint32Array(1);
for (let i = 0; i < 10; i++) {
crypto.getRandomValues(buf);
if (buf[0] < limit) return buf[0] % max;
}
return buf[0] % max; // 降级
}
当 max 不整除 2^32 时,直接取模会让 [0, remainder) 的值多出现一次,引入偏差。limit 是 2^32 中 max 的最大整数倍,随机值 < limit 时取模无偏差;≥ limit 时拒绝并重新采样。
crypto.getRandomValues vs Math.random:
| 特性 | Math.random | crypto.getRandomValues |
|---|---|---|
| 随机性质量 | 伪随机(PRNG) | 加密级随机(CSPRNG) |
| 可预测性 | 理论可预测 | 不可预测 |
| 适用场景 | 模拟、动画 | 抽奖、安全、密钥 |
| 性能 | 更快 | 稍慢 |
对于抽奖等公平性要求高的场景,必须使用 crypto.getRandomValues。
四、排序选项的组合策略
大小写敏感
- 开启:
Apple≠apple(大写排前面,因为 A=65 < a=97) - 关闭:
Apple=apple(比较时统一转小写) - 排序人名、地名时建议关闭,结果更符合直觉
去除空行
排序前过滤空行,减少干扰。注意:如果同时开启 trim,纯空格行也会被过滤。
排序后去重
排序完成后去除重复行,只保留唯一值。去重使用 Set 实现,保留排序后的位置。
组合示例
| 场景 | 推荐组合 |
|---|---|
| 名字按字母排列 | 字母升序 + 关闭大小写敏感 + 去除空行 |
| 版本号排序 | 自然排序 + 去除空行 |
| 日志按时间排序 | 数值升序 + 去除空行 |
| 找最长行 | 长度降序 |
| 随机抽奖顺序 | 随机打乱 + 去除空行 + 排序后去重 |
| 关键词去重排列 | 字母升序 + 排序后去重 + 去除空行 |
五、排序算法的复杂度分析
| 排序模式 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 字母/数值/长度排序 | O(n log n) | O(n) | Array.sort 底层为 TimSort |
| 自然排序 | O(n log n × m) | O(n) | m 为平均段数,拆分增加常数 |
| 随机打乱 | O(n) | O(1) | Fisher-Yates 原地交换 |
对于 10000 行以内的典型输入,所有排序模式都在 10ms 内完成,无需考虑性能瓶颈。
六、总结
文本排序看似简单,但选择正确的排序模式对结果正确性至关重要:
- 字母排序适合纯文本,但会错误排列嵌入数字的字符串
- 自然排序是文件名、版本号的正确选择,核心是”拆分交替比较”
- 随机打乱必须用 Fisher-Yates + crypto 才能保证公平性
- 选项组合(大小写敏感、去除空行、去重)提供了灵活的数据处理能力
立即体验:文本排序工具 — 8 种排序模式,全本地处理