Skip to content

外部排序

为什么需要外部排序

当待排序的数据量非常大,无法一次性全部装入内存时,就不能使用内部排序算法(如快排、归并等)。例如一个 10GB 的文件,而内存只有 512MB,此时必须借助外存(磁盘)来完成排序,这就是外部排序。

外部排序的核心瓶颈在于磁盘 I/O,因为磁盘读写速度远低于内存操作。因此,外部排序算法的设计目标是尽量减少磁盘 I/O 次数

核心思想

外部排序的基本策略:

  • 生成初始归并段(Run):将大文件分成若干段,每段读入内存进行内部排序,排好序后写回外存,形成有序的初始归并段
  • 多路归并:对所有初始归并段进行多趟归并,每趟将若干个有序段合并为更大的有序段,直到整个文件有序
  • 优化方向:减少归并趟数、减少每趟的 I/O 次数、加速归并过程中的比较操作

外部排序的总 I/O 次数 = 初始归并段生成的 I/O + 归并阶段的 I/O。归并阶段的 I/O 次数与归并趟数直接相关,因此减少趟数是关键。

交互可视化

通过下方的交互动画,你可以逐步观察外部排序的执行过程:

加载可视化中...

操作详解

外部排序的概念

外部排序的基本流程分为两个阶段:

  1. 生成初始归并段:假设文件共有 N 条记录,内存工作区可容纳 M 条记录。每次读入 M 条记录到内存,用内部排序算法排好序后写回磁盘,共生成 m = ⌈N/M⌉ 个初始归并段
  2. 多路归并:对 m 个初始归并段进行 k 路归并,每趟将 k 个有序段合并为一个更大的有序段,经过多趟归并后得到最终有序文件
文件(N条记录)
  ↓ 内部排序
[段1] [段2] [段3] ... [段m]    ← m个初始归并段
  ↓ k路归并(第1趟)
[大段1] [大段2] ...            ← ⌈m/k⌉个归并段
  ↓ k路归并(第2趟)
[更大段1] ...                  ← 段数继续减少
  ↓ ...
[最终有序文件]

多路归并

设有 m 个初始归并段,采用 k 路归并,则归并趟数为:

S = ⌈log_k(m)⌉

这是外部排序最核心的公式。减少归并趟数 S 的两种途径:

策略方法效果
增大归并路数 k使用多路归并(如 5 路、10 路)S = ⌈log_k(m)⌉ 随 k 增大而减小
减少初始归并段数 m使用置换-选择排序生成更长的归并段m 减小,S 随之减小

但增大 k 也有代价:在 k 路归并中,每次从 k 个段的当前元素中选最小值,简单方法需要 k-1 次比较,k 越大比较次数越多。解决方案是使用败者树,将每次选最小值的比较次数降为 ⌈log₂k⌉

每趟归并需要将所有记录读一遍、写一遍,I/O 次数与数据总量成正比。因此:

总 I/O 次数 ∝ 归并趟数 S = ⌈log_k(m)⌉

败者树

败者树是一棵完全二叉树,用于加速 k 路归并中"从 k 个元素中选最小值"的过程。

工作原理:

  • 叶子结点存放 k 个归并段的当前元素
  • 内部结点记录"败者"(较大者)的来源编号
  • 根结点之上额外有一个结点记录"冠军"(最小者)
  • 每次取走最小元素后,只需沿该元素所在叶子到根的路径进行调整,比较次数为 ⌈log₂k⌉

败者树的关键优势:

选最小值方式比较次数适用场景
直接比较k-1k 较小时可用
败者树⌈log₂k⌉k 较大时显著优于直接比较

例如 8 路归并:直接比较需 7 次,败者树只需 3 次。k 越大,败者树的优势越明显。

置换-选择排序

普通方法生成的初始归并段长度等于内存工作区大小 M。置换-选择排序可以生成平均长度为 2M 的初始归并段,从而减少归并段数量 m。

算法步骤:

  1. 从文件读入 M 条记录填满内存工作区(设为 WA)
  2. 从 WA 中选出最小的且不小于上一个输出记录的记录,输出到当前归并段
  3. 从文件读入下一条记录填补 WA 空位
  4. 重复步骤 2-3,直到 WA 中所有记录都小于上一个输出记录
  5. 当前归并段结束,开始生成下一个归并段
  6. 重复以上过程直到文件处理完毕

置换-选择排序生成的各个归并段长度通常不等,这引出了最佳归并树的问题。

最佳归并树(类似哈夫曼树):

当各归并段长度不等时,归并顺序会影响总 I/O 次数。为使总 I/O 次数最少,应构造类似哈夫曼树的最佳归并树:

  • 将每个归并段的长度作为叶子结点的权值
  • 构造 k 叉哈夫曼树,权值大的归并段靠近根(晚参与归并)
  • 带权路径长度最小 → 总 I/O 次数最少

注意:若初始归并段数量不足以构成严格的 k 叉哈夫曼树,需要补充长度为 0 的虚段。补虚段的条件:设 m 个初始归并段进行 k 路归并,若 (m-1) % (k-1) ≠ 0,则需补 k-1-(m-1)%(k-1) 个虚段。

复杂度分析

指标公式/值说明
归并趟数S = ⌈log_k(m)⌉m 为初始归并段数,k 为归并路数
每趟 I/O 次数2 × ⌈N/B⌉N 为记录总数,B 为磁盘块容量(一读一写)
总 I/O 次数S × 2 × ⌈N/B⌉归并阶段的总磁盘读写次数
败者树每次选最小值⌈log₂k⌉ 次比较相比直接比较的 k-1 次显著减少
置换-选择排序平均段长2MM 为内存工作区大小

关键结论:外部排序的时间开销主要取决于磁盘 I/O 次数,而 I/O 次数与归并趟数成正比。

考研高频考点

  • ⭐ 归并趟数公式 S = ⌈log_k(m)⌉ 的计算(选择题/填空题必考)
  • ⭐ 增大归并路数 k 与减少初始归并段数 m 对趟数的影响(选择题高频)
  • ⭐ 败者树的作用及比较次数 ⌈log₂k⌉(选择题/简答题)
  • ⭐ 置换-选择排序生成初始归并段的过程(大题偶尔考)
  • ⭐ 最佳归并树(k 叉哈夫曼树)的构造与虚段补充规则(大题高频)
  • 外部排序总 I/O 次数的计算(填空题)
  • 内部归并排序 vs 外部归并排序的区别(概念题)