置换-选择排序与最佳归并树
这两节是「减少归并段个数
- 置换-选择排序:让初始归并段变长,从而变少。
- 最佳归并树:归并段长度不等时,安排归并顺序使 I/O 次数最少。
两节都是纯手算,且都能算出一个确定的数字,所以特别适合出计算题。
机制
为什么需要置换-选择
由 8.7.2 可知,减少初始归并段个数
采用内部排序算法得到的各个初始归并段长度都相同(除最后一段外),它依赖于内部排序时可用内存工作区的大小。 因此,必须探索新的方法,用来产生更长的初始归并段——这就是置换-选择算法。
置换-选择算法
设初始待排文件为 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 为空。由此得到全部初始归并段。
教材的例子(表 8.2):
| 输出文件 FO | 工作区 WA | 输入文件 FI |
|---|---|---|
| — | — | 17, 21, 05, 44, 10, 12, 56, 32, 29 |
| — | 17 21 05 | 44, 10, 12, 56, 32, 29 |
| 05 | 17 21 44 | 10, 12, 56, 32, 29 |
| 05 17 | 10 21 44 | 12, 56, 32, 29 |
| 05 17 21 | 10 12 44 | 56, 32, 29 |
| 05 17 21 44 | 10 12 56 | 32, 29 |
| 05 17 21 44 56 | 10 12 32 | 29 |
| 05 17 21 44 56 # | 10 12 32 | 29 |
| 10 | 29 12 32 | — |
| 10 12 | 29 32 | — |
| 10 12 29 | 32 | — |
| 10 12 29 32 | — | — |
| 10 12 29 32 # | — | — |
结果:两个初始归并段
边界辨析:
WA 只能装 3 个记录,却产出了长度为 5 的归并段——这正是置换-选择的全部价值: 归并段的长度不再受内存工作区大小的限制。 关键在第 5 步的「比 MINIMAX 大」这个条件:新读进来的记录只要比刚输出的那个大, 就还能接在当前归并段后面;比它小的只能留给下一段(表中被划掉的 10、12、32 就是这样攒出第二段的)。
教材末尾还有一句:「在 WA 中选择 MINIMAX 记录的过程需利用败者树来实现。」 ——败者树 在这里第二次出现,用途从「归并选最小」变成「工作区选 MINIMAX」。
最佳归并树
文件经过置换-选择排序后,得到的是长度不等的初始归并段。如何组织长度不等的初始归并段的归并顺序,使得 I/O 次数最少?
在归并树中,各叶结点表示一个初始归并段,上面的权值表示该归并段的长度;叶结点到根的路径长度表示其参加归并的趟数;各非叶结点代表归并成的新归并段;根结点表示最终生成的归并段。树的带权路径长度 WPL 为归并过程中的总读记录数,所以 I/O 次数
教材的例子:由置换-选择排序得到 9 个初始归并段,长度依次为
| 方案 | 内部结点(新归并段长度) | WPL | I/O 次数 |
|---|---|---|---|
| 图 8.18 任意 3 路平衡归并 | 242 | 484 | |
| 图 8.19 最佳归并树 | 223 | 446 |
「显然,归并方案不同,所得归并树不同,树的带权路径长度(I/O 次数)亦不同。为了优化归并树的 WPL,可以将哈夫曼树的思想推广到
虚段:不足以构成严格 叉树时
图 8.19 中的哈夫曼树是一棵严格 3 叉树,即树中只有度为 3 或 0 的结点。
若只有 8 个初始归并段(上例中少了一个长度为 30 的归并段):
- 错误做法:缺额的归并段留在最后,即除最后一次做二路归并外,其他各次归并仍是 3 路归并——此归并方案的 I/O 次数为 386。「显然,这不是最佳方案。」
- 正确的做法:若初始归并段不足以构成一棵严格
叉树(也称正则 叉树)时,则需添加长度为 0 的「虚段」,按照哈夫曼树的原则,权为 0 的叶子应离树根最远。此时的 I/O 次数仅为 326。
添加虚段数目的判定(设度为 0 的结点有
因此,对严格
- 若
,则说明这 个叶结点(初始归并段)正好可以构造 叉归并树。此时内结点有 个。 - 若
,则说明对于这 个叶结点,其中有 个多余,不能包含在 叉归并树中。为构造包含所有 个初始归并段的 叉归并树,应在原有 个内结点的基础上再增加 1 个内结点,并补充 个虚段。
验算教材例子:
层次辨析:
这里的「三式」与 5.1.3 的三式联立是同一套东西: 总结点数 = 各度结点数之和;总结点数 = 总分支数 + 1。 区别只是这里限定了「严格
叉」,即 ,于是两式就能解出 。 第 5 章的公式在第 8 章直接兑现,不必当成新知识。
手算模板
置换-选择排序:
- 画三列表:FO | WA | FI。
- 先读
个进 WA。 - 每轮:在 WA 中「比上一个输出值大」的记录里选最小,输出到 FO;从 FI 补一个进 WA。
- 若 WA 中没有比上一个输出值大的记录了 → 当前归并段结束,写
#,然后从 WA 全体里重新选最小开始新段。 - FI 空后继续排空 WA。
最佳归并树:
- 数初始归并段个数
和路数 。 - 算
: 不补; 补 个长度为 0 的虚段。 - 按哈夫曼树的方法建
叉树:每次取最小的 个合并,新结点权 = 之和。虚段(权 0)自然会落在最深处。 。 。
边界
| 说法 | 判断 | 说明 |
|---|---|---|
| 「初始归并段长度不能超过内存工作区」 | ❌ | 置换-选择可以产生更长的归并段(例中 WA=3 却得到长度 5) |
| 「置换-选择每段长度相同」 | ❌ | 长度不等,这正是需要最佳归并树的原因 |
| 「MINIMAX 是 WA 中的最小值」 | ⚠️ | 是比上一个输出值大的记录中的最小值;只有段首才是全体最小 |
| 「置换-选择用堆实现」 | ⚠️ | 教材原话是**「需利用败者树来实现」** |
| 「I/O 次数 = WPL」 | ❌ | |
| 「WPL 要加上叶结点的权」 | ❌ | WPL |
| 「归并段不够时最后做一次少路归并即可」 | ❌ | 不是最佳方案(例中 386 > 326) |
| 「虚段数 | ❌ | 是 |
| 「虚段可以放在任意位置」 | ❌ | 权为 0 的叶子应离树根最远 |
| 「最佳归并树是普通哈夫曼树」 | ⚠️ | 是推广到 |
口径差异:
置换-选择和最佳归并树在算法竞赛里完全没有对应物,它们解决的是「磁盘慢、内存小」这个 1970 年代的问题。 但 408 年年在考:置换-选择生成初始归并段的实例(2023)、构造三叉哈夫曼树及相关的分析和计算(2013)。 这一节的好处是答案唯一且可验算——I/O 次数是个确定的整数,算完可以用
复核。
对照速查
| 量 | 式子 |
|---|---|
| 严格 | |
| 判虚段 | |
| WPL | |
| I/O 次数 |
| 教材例子 | 段数 | 方案 | I/O |
|---|---|---|---|
| 9 | 任意 3 路平衡归并 | 484 | |
| 同上 | 9 | 最佳归并树 | 446 |
| 去掉 30 | 8 | 缺额留最后做二路 | 386 |
| 同上 | 8 + 1 虚段 | 最佳归并树 | 326 |
| 减少 I/O 的两条路 | 对应节 |
|---|---|
| 增大归并路数 | 8.7.3 败者树 |
| 减少归并段个数 | 8.7.4 置换-选择 |
| 段长不等时安排顺序 | 8.7.5 最佳归并树 |
考点
- 置换-选择排序生成初始归并段的实例(2023 命题追踪)——画三列表。
- MINIMAX 的选取条件(比上一个输出值大的最小者)。
- 构造三叉哈夫曼树及相关的分析和计算(2013 命题追踪)。
- 虚段数目的判定公式。
- I/O 次数
。 - 权为 0 的虚段应离根最远。
- 「缺额留最后做低路归并」不是最佳方案。
链接
- 🏠 返回总览:数据结构第 8 章:排序总览
- ⬅️ 上一节:8.7.1~8.7.3 外部排序与败者树
- 🔗 哈夫曼树与 WPL:5.5.1 哈夫曼树和哈夫曼编码
- 🔗 三式联立的来源:5.1.3 树的性质
- 🔗 全章总账表:8.6 各种内部排序算法的比较及应用
- 📖 名词库:第 8 章名词库