自然两路合并排序 (Natural Two-Way Merge Sort)
049外部自然合并
故事:老图书馆员的智慧
在古老的图书馆深处,住着一位名叫艾德温的老馆员。他管理着成千上万的书籍,每天都要将它们按编号整理。年轻的学徒们总是用最笨的方法——一本一本地比较、交换,直到所有书排好序。
但艾德温有一个秘密武器。
"你们看,"他指着书架上已经部分有序的书,“这些书,虽然整体混乱,但仔细看,这里1-2-3-4是连续的,那里7-8-9也是有序的。为什么要破坏这些自然的秩序呢?”
艾德温的方法很简单:
- 识别自然段:他沿着书架走,每当发现编号开始下降时,就知道一个"自然段"(natural run)结束了
- 交替存放:把这些自然段交替放到两个推车上
- 合并:每次从两个推车各取一个段,像洗牌一样合并,放回书架
"如果数据本身就有序,"艾德温笑道,“我们一趟都不用走。”
算法原理
自然两路合并排序是外部排序的经典算法,源自 TAOCP 第3卷第5.4.1节。
核心思想
与传统归并排序强制从长度为1的段开始不同,自然合并排序利用数据中已存在的有序性:
- 自然段(Natural Run):数组中连续的升序子序列
- 初始分布时识别这些自然段,交替放入两条"磁带"
- 反复合并,直到只剩一个有序段
算法流程
输入: [3, 1, 4, 1, 5, 9, 2, 6] 第0步 - 识别自然段: [3] [1,4] [1,5,9] [2,6] 交替分布: 磁带A: [3] [1,5,9] 磁带B: [1,4] [2,6] 第1步 - 合并: 合并 [3] 和 [1,4] → [1,3,4] 合并 [1,5,9] 和 [2,6] → [1,2,5,6,9] 交替分布: 磁带A: [1,3,4] 磁带B: [1,2,5,6,9] 第2步 - 合并: 合并 [1,3,4] 和 [1,2,5,6,9] → [1,1,2,3,4,5,6,9] 完成!优势
| 情况 | 传统归并 | 自然归并 |
|---|---|---|
| 完全逆序 | log₂n 趟 | log₂n 趟 |
| 部分有序 | log₂n 趟 | 更少趟数 |
| 完全有序 | log₂n 趟 | 0 趟 |
复杂度分析
- 时间复杂度: O(n log n) 最坏情况,O(n) 最好情况(已排序)
- 空间复杂度: O(n) 需要额外存储空间
- 趟数: 与初始 run 数相关,run 越少趟数越少
代码实现要点
- 磁带模拟:用数组+位置指针模拟磁带读写
- Run 识别:遍历数组,遇到下降即为一个 run 结束
- 合并策略:每趟合并两个磁带上的 runs,交替输出
历史意义
自然合并排序诞生于磁带机时代,当时外部存储访问成本极高。利用数据的自然有序性可以显著减少磁带读写次数,这在当时意味着节省大量时间和金钱。
即使在今天的内存排序中,识别自然 runs 的思想仍然有价值——TimSort(Python的排序算法)就采用了类似策略。