排序算法之:多路平衡归并排序(Multiway Balanced Merge Sort)在外部排序中的败者树优化
题目描述:
假设你有一个非常大的文件(比如 100GB),包含大量记录需要按关键字排序,但内存有限(比如只有 2GB)。请设计一个基于多路平衡归并的外部排序算法,并利用败者树(Loser Tree)优化多路归并过程,减少关键字的比较次数。
解题过程:
-
外部排序的基本挑战
当数据量远大于内存容量时,无法一次性将所有数据读入内存排序。外部排序的核心思想是“分而治之”:先将大文件分割成若干能装入内存的小块,每块单独排序后写回外存,再通过多路归并逐步合并成有序文件。 -
多路平衡归并的初步设计
-
阶段一:生成初始有序段(Run Generation)
将大文件依次读入内存,每次读入尽可能多的数据(如 2GB),用高效的内排序算法(如快速排序)排序,生成一个有序段(Run)并写回磁盘。假设文件总大小 100GB,内存 2GB,则会生成 50 个初始有序段(每个约 2GB)。 -
阶段二:多路归并(K-Way Merge)
从 K 个有序段中各读入一部分数据到内存缓冲区(缓冲区总大小不超过内存容量),比较每个有序段当前的最小值,选出全局最小值输出到最终文件,并从对应有序段补入新数据。重复直到所有有序段处理完毕。
-
-
直接多路归并的问题
若直接进行 K 路归并,每选出一个最小值需比较 K-1 次(遍历所有段的最小值)。总比较次数为 O(N·K),当 K 较大时(如 K=50),比较操作会成为瓶颈。 -
败者树(Loser Tree)的引入
败者树是一种完全二叉树,用于优化多路归并的比较过程:- 叶子节点:存储 K 个有序段当前的最小值(即参与比较的关键字)。
- 内部节点:记录“失败者”(即比较中较大的值),而胜者(最小值)向上传递。
- 根节点:记录最终胜者(全局最小值)及其来源有序段。
-
败者树的构建与调整步骤
-
初始化:
从 K 个有序段各读入一个记录到叶子节点,自底向上构建败者树:- 比较兄弟节点的关键字,将较大者(败者)存入父节点,较小者(胜者)继续向上比较。
- 重复直到根节点,根节点记录当前全局最小值的来源段号(非关键字本身)。
例如,K=4 时,初始化需 3 次比较(K-1 次)。
-
输出与调整:
- 输出根节点对应的最小值(如段1的最小值)。
- 从段1补入新记录到对应叶子节点,沿路径向上调整:
仅需比较新记录与当前路径上的败者,更新局部胜败关系。
例如,调整一次仅需 log₂K 次比较(树高)。 - 重复直到所有记录输出。
-
-
败者树的优势分析
- 初始化后,每选出一个最小值仅需 log₂K 次比较(而非 K-1 次)。
例如 K=50 时,log₂50≈5.64,比较次数从 49 次降至 6 次左右。 - 总比较次数从 O(N·K) 优化为 O(N·logK),显著提升归并效率。
- 初始化后,每选出一个最小值仅需 log₂K 次比较(而非 K-1 次)。
-
实际应用中的细节处理
- 缓冲区管理:为每个有序段分配固定大小的输入缓冲区,避免频繁磁盘 I/O。
- 归并轮数:若初始有序段数 K 大于内存可支持的最大归并路数,需多轮归并(如第一轮 50 路归并生成更大的有序段,第二轮继续合并)。
- 稳定性:败者树保持归并的稳定性,当关键字相同时,优先输出先入树的记录。
通过败者树优化,多路平衡归并排序在外部排序中能高效处理海量数据,平衡 I/O 与计算开销。