6.外部排序
前面介绍过的排序方法都是在内存中进行的(称为内部排序)。而在许多应用中,经常需要对大文件进行排序,因为文件中的记录很多,无法将整个文件复制进内存中进行排序。因此,需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之问的交换。这种排序方法就称为外部排序。
外部排序的方法
- 外部排序过程中的时问代价主要考虑访问磁盘的次数,即
I/O次数。 - 外部排序通常采用归并排序法
在外部排序中实现两两归并时,由于不可能将两个有序段及归并结果段同时存放在内存中, 因此需要不停地将数据读出、写入磁盘,而这会耗费大量的时间,一般情况下:
外部排序的总时间=内部排序所需的时问 +外存信息读写的时间+内部归并所需的时间
显然,外存信息读写的时间远大于内部排序和内部归并的时间,因此应着力减少 I/O 次数。
一般地, 对 个初始归并段, 做 路平衡归并, 归并树可用严格 叉树 (即只有度为 与度为 0 的 结点的 叉树)来表示。第一趟可将 个初始归并 段归并为 个归并段, 以后每趟归并将 个归并 段归并成 个归并段, 直至最后形成一个大的归并段为止。
可见, 只要增大归并路数 , 或减少初始归并段个数 , 都能减少归并趟数 , 进而减少读写磁盘的次数, 达到提高外部排序速度的目的。
多路平衡归并与败者树
提示
做内部归并时, 在 个元素中选择关键字最小的记录需 要比较 次。每趟归并 个元素需要做 次比较, 趟归并总共需要的比较次 数为
式中, 随 增长而增长, 因此内部归并时间亦随 的增长而增长。这将抵消由于增 大 而减少外存访问次数所得到的效益。因此, 不能使用普通的内部归并排序算法。
为了使内部归并不受归并路数的增大的影响, 引入了败者树。败者树是树形选择排序的一种变体, 可视为一棵完全二叉树。 个叶结点分别存放 个归并段在归并过程中当前参加比较的记录, 内部结点用来记忆左右子树中的 “失败者”, 而让胜者往上继续进行比较, 一直到根结点。
败者树实例
若比较两个数, 大的为失败者、小的为胜利者, 则根结点指向的数为最小数。

之所以在父节点中存储的是失败者的段号,是因为这样当新加入一个元素时,就可以直接通过父节点中的段号与上一轮的失败者比较
因为 路归并的败者树深度为 , 因此 个记录中选择最小关键字, 最多需要 次 比较。所以总的比较次数为
可见, 使用败者树后, 内部归并的比较次数与 无关了。因此, 只要内存空间允许, 增大归 并路数 将有效地减少归并树的高度, 从而减少 次数, 提高外部排序的速度。
值得说明的是, 归并路数 并不是越大越好。归并路数 增大时, 相应地需要增加输入缓 冲区的个数。若可供使用的内存空间不变, 势必要减少每个输入缓冲区的容量, 使得内存、外 存交换数据的次数增大。当 值过大时,虽然归并趟数会减少,但读写外存的次数仍会增加。
败者树和传统简单选择比较次数对比
置换-选择排序
设初始待排文件为 FI, 初始归并段输出文件为 FO, 内存工作区为 WA, FO 和 WA 的初始状 态为空, WA 可容纳 个记录。置换-选择算法的步骤如下:
- 从 FI 输入 个记录到工作区 WA。
- 从 WA 中选出其中关键字取最小值的记录, 记为 MINIMAX 记录。
- 将 MINIMAX 记录输出到 FO 中去。
- 若 FI 不空, 则从 FI 输入下一个记录到 WA 中。
- 从 WA 中所有关键字比 MINIMAX 记录的关键字大的记录中选出最小关键字记录, 作为 新的 MINIMAX 记录。
- 重复 3) 5), 直至在 WA 中选不出新的 MINIMAX 记录为止, 由此得到一个初始归并段, 输出一个归并段的结束标志到 FO 中去。
- 重复 2)~6), 直至 WA 为空。由此得到全部初始归并段。
算法实例
设待排文件 , WA 容量为 3 。排序过程如表所示。

在WA 中选择MINIMAX 记录的过程需利用败者树来实现
最佳归并树
置换置换置换置换置换置换置换置换置换-选择排序)后,得到的是长度不等的初始归并段
叶结点到根的路径长度表示其参加归并的趟数,各非叶结点代表归并成的新归并段,根结点表示最终生成的归并段。树的带权路径长度 WPL 为归并过程中的总读记录数,故
为了优化归并树的,可以将哈夫曼树的思想推广到叉树的情形,在归并树中,让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总的I/O次数最少的最佳归并树。
若初始归并段不足以构成一棵严格叉树时,需添加长度为0的虚段,按照哈夫曼树的原则,杈为0 的叶子应离树根最远。
🤔如何判定添加虚段的数目?
叉的最佳归并树一定是一棵严格的 叉树, 即树中只包含度为 、度为 0 的结点。 设度为 的结点有 个, 度为 0 的结点有 个, 归并树总结点数 则:
- 若 , 则说明这 个叶结点(初始归并段)正好可以构 造 叉归并树。此时, 内结点有 个。
- 若 , 则说明对于这 个叶结点, 其中有 个多余, 不能包含在 叉 归并树中。为构造包含所有 个初始归并段的 叉归并树, 应在原有 个内结点的基础 上再增加 1 个内结点。它在归并树中代替了一个叶结点的位置, 被代替的叶结点加上刚才 多出的 个叶结点, 即再加上 个空归并段[1], 就可以建立归并树。
就是让上面那个式子可以整除即可 ↩︎
