文本排序完全指南:8 种排序模式与自然排序原理

排序:计算机科学的基础问题

排序是计算机科学中最基础也最实用的问题之一。从整理名单到组织数据,从版本号对齐到随机抽取,排序无处不在。但”排序”远不止”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.10v1.2.9

  • 字母排序v1.2.10 < v1.2.9(因为 “1” < “9”)
  • 自然排序v1.2.10 > v1.2.9(因为 10 > 9)

算法步骤

  1. 拆分:用正则 /(\\d+|\\D+)/g 将字符串拆分为交替的数字段与非数字段
  2. 逐段比较:对齐两个字符串的段,逐段比较
    • 数字段(/^\\d+$/):用 BigInt 按数值比较
    • 文本段:按字符串比较
  3. 长度兜底:如果所有对齐段都相等,较短的段序列排在前面

边界处理

  • 空字符串排在最前
  • 纯数字行用 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) 的值多出现一次,引入偏差。limit2^32max 的最大整数倍,随机值 < limit 时取模无偏差;≥ limit 时拒绝并重新采样。

crypto.getRandomValues vs Math.random

特性Math.randomcrypto.getRandomValues
随机性质量伪随机(PRNG)加密级随机(CSPRNG)
可预测性理论可预测不可预测
适用场景模拟、动画抽奖、安全、密钥
性能更快稍慢

对于抽奖等公平性要求高的场景,必须使用 crypto.getRandomValues

四、排序选项的组合策略

大小写敏感

  • 开启Appleapple(大写排前面,因为 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 内完成,无需考虑性能瓶颈。

六、总结

文本排序看似简单,但选择正确的排序模式对结果正确性至关重要:

  1. 字母排序适合纯文本,但会错误排列嵌入数字的字符串
  2. 自然排序是文件名、版本号的正确选择,核心是”拆分交替比较”
  3. 随机打乱必须用 Fisher-Yates + crypto 才能保证公平性
  4. 选项组合(大小写敏感、去除空行、去重)提供了灵活的数据处理能力

立即体验:文本排序工具 — 8 种排序模式,全本地处理