外部排序可能会考查相关概念、方法和排序过程,外部排序的算法比较复杂,不会在算法设计上进行考查。
一、外部排序的基本概念与方法
外部排序指待排序文件较大,内存一次放不下,需存放在外存的文件的排序。
1. 基本概念
在许多应用中,经常需要对大文件进行排序,因为文件中的记录很多,无法将整个文件复制进内存中进行排序。因此,需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。这种排序方法就称为外部排序 。
2. 方法
文件通常是按块存储在磁盘上的,操作系统也是按块对磁盘上的信息进行读写的。因为磁盘读/写的机械动作所需的时间远远超过内存运算的时间(相比而言可以忽略不计),因此在外部排序过程中的时间代价主要考虑访问磁盘的次数,即 I/O 次数。
外部排序通常采用归并排序法。它包括两个阶段:
① 根据内存缓冲区大小,将外存上的文件分成若干长度为 t 的子文件,依次读入内存并利用内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写回外存,称这些有序子文件为 归并段或顺串 ;
② 对这些归并段进行逐趟归并,使归并段(有序子文件)逐渐由小到大,直至得到整个有序文件为止。
二、多路平衡树(Multiway Balanced Tree)与败者树(Loser Tree)
为减少平衡归并中外存读写次数所采取的方法:增大归并路数和减少归并段个数。利用败者树增大归并路数。
1. 多路平衡树的算法思想
多路平衡树是一种树数据结构,它允许每个结点有多个子结点,通常用于数据库和文件系统中。它的目的是保持数据的平衡、减少搜索时间,并提高插入和删除操作的效率。
2. 多路平衡树的示例
一般地,对 r 个初始归并段,做 k 路平衡归并(即每趟将 k 个或 k 个以下的有序子文件归并成一个有序子文件)。第一趟可将 r 个初始归并段归并为 ⌈r / k⌉ 个归并段,以后每趟归并将 m 个归并段归并成 ⌈m / k⌉ 个归并段,直至最后形成一个大的归并段为止。树的高度 - 1 = ⌈logkr⌉ = 归并趟数 S 。可见,只要增大归并路数 k,或减少初始归并段个数 r,都能减少归并趟数 S,进而减少读写磁盘的次数,达到提高外部排序速度的目的。
增加归并路数 k 能减少归并趟数 S,进而减少 I/O 次数。然而,增加归并路数 k 时,内部归并的时间将增加。做内部归并时,在 k 个元素中选择关键字最小的记录需要比较 k - 1 次。每趟归并 n 个元素需要做 (n - 1) × (k - 1) 次比较, S 趟归并总共需要的比较次数为:
S × (n - 1) × (k - 1) = ⌈logkr⌉ × (n - 1) × (k - 1) = ⌈log2r⌉ × (n - 1) × (k - 1) / ⌈log2k⌉。
式中,(k - 1) / ⌈log2k⌉ 随着 k 的增长而增长,因此内部归并时间亦随k 的增长而增长。这将抵消由于增大 k 而减少外存访问次数所得到的效益。因此,不能使用普通的内部归并排序算法。为了使内部归并不受 k 的增大的影响,引入了 败者树 。
3. 败者树的算法思想
败者树是一种用于高效合并 k 个有序序列的完全二叉树。它由 k 个外部结点和 k - 1 个内部结点构成,每一个内部结点记录的是在该结点比赛的输者,根结点保存的是“赢家”。
4. 败者树的示例
败者树是树形选择排序的一种变体,可视为一棵完全二叉树。k 个叶结点分别存放 k 个归并段在归并过程中当前参加比较的记录,内部结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。若比较两个数,大的为失败者、小的为胜利者,则根结点指向的数为最小数。
因为 k 路归并的败者树深度为 ⌈log2k⌉,因此 k 个记录中选择最小关键字,最多需要 ⌈log2k⌉ 次
比较。所以总的比较次数为:
S × (n - 1) × ⌈log2k⌉ = ⌈logkr⌉ × (n - 1) × ⌈log2k⌉ = ⌈log2r⌉ × (n - 1) 。
可见,使用败者树后,内部归并的比较次数与 k 无关了。因此,只要内存空间允许,增大归并路数 k 将有效地减少归并树的高度,从而减少 I / O 次数,提高外部排序的速度。
值得说明的是,归并路数 k 并不是越大越好。归并路数K 增大时,相应地需要增加输入缓冲区的个数。若可供使用的内存空间不变,势必要减少每个输入缓冲区的容量,使得内存、外存交换数据的次数增大。当K 值过大时,虽然归并趟数会减少,但读写外存的次数仍会增加。
5. 例题
① 多路平衡归并的作用是( A )。
A. 减少归并趟数
B. 减少初始归并段的个数
C. 便于实现败者树
D. 以上都对
② 在下列关于外部排序过程输入/输出缓冲区作用的叙述中,不正确的是( D )。
A. 暂存输入/输出记录
B. 内部归并的工作区
C. 产生初始归并段的工作区
D. 传送用户界面的消息
③ 在由 k 路归并构建的败者树中选取一个关键字最小的记录,则所需时间为( C )。
A. O(1)
B. O(k)
C. O(logk)
D. 以上都不对
④ 下列关于小顶堆和败者树的说法中,正确的是( C )。
I. 败者树从下往上维护,每上一层,只需和失败结点比较 1 次
II. 败者树的每次维护,必定要从叶结点一直走到根结点,不可能从中间停止
III. 堆从上往下维护,每下一层,若其左右孩子均不为空,则需比较 2 次
IV. 堆的每次维护,必定要从根结点一直走到叶结点,不可能从中间停止
A. I、III
B. II、III
C. I、II、III
D. I、III、IV
三、置换-选择排序(生成初始归并段)(Replacement Selection Sort)
利用置换-选择排序增大归并段长度来减少归并段个数。
1. 算法思想与实现步骤
1)算法思想:
由上述讨论可知,减少初始归并段个数 r 也可以减少归并趟数 S 。若总的记录个数为 n,每个归并段的长度为 t,则归并段的个数 r = ⌈n / t⌉ 。采用内部排序方法得到的各个初始归并段长度都相同(除最后一段外),它依赖于内部排序时可用内存工作区的大小。因此,必须探索新的方法,用来产生更长的初始归并段,这就是下面要介绍的置换-选择算法。
2)实现步骤:
设初始待排文件为 FI,初始归并段输出文件为 FO,内存工作区为 WA,FO 和 WA 的初始状态为空, WA 可容纳 w 个记录。
① 从 FI 输入 w 个记录到工作区 WA 。
② 从 WA 中选出其中关键字取最小值的记录,记为 MINIMAX 记录。
③ 将 MINIMAX 记录输出到 FO 中去。
④ 若 FI 不空,则从 FI 输入下一个记录到 WA 中。
⑤ 从 WA 中所有关键字比 MINIMAX 记录的关键字大的记录中选出最小关键字记录,作为新的 MINIMAX 记录。
⑥ 重复 ③-⑤,直至在 WA 中选不出新的 MINIMAX 记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到 FO 中去。
⑦ 重复 ②-⑥,直至 WA 为空。由此得到全部初始归并段。
【注:上述算法,在 WA 中选择 MINIMAX 记录的过程需利用败者树来实现。】
2. 示例
3. 例题
① 置换-选择排序的作用是( A )。
A. 用于生成外部排序的初始归并段
B. 完成将一个磁盘文件排序成有序文件的有效的外部排序算法
C. 生成的初始归并段的长度是内存工作区的 2 倍
D. 对外部排序中输入/归并/输出的并行处理
② 一个无序文件的 n 个记录采用置换-选择排序产生 m 个有序段,则 m 和 n 的关系是( D )。
A. m 与 n 成正比
B. m = log2n
C. m 与 n 成反比
D. 以上都不对
四、最佳归并树(Optimal Merge Tree)
由长度不等的归并段,进行多路平衡归并,需要构造最佳归并树。
1. 算法思想
文件经过置换-选择排序后,得到的是长度不等的初始归并段。在归并树中,让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总的 I / O 次数最少的最佳归并树。(将哈夫曼树的思想推广到 m 叉树,以优化归并树的 WPL 。)
若初始归并段不足以构成一棵严格 k 叉树(也称正则 k 叉树)时,需添加长度0 的“虚段“,按照哈夫曼树的原则,权为 0 的叶子应离树根最远。那么如何判定添加虚段的数目?
正则 k 叉树:树中每个分支结点都有 k 个孩子,即树中只有度为 0 或 k 的结点。
设度为 0 的结点有 n0 个,度为 k 的结点有 nk 个,归并树的结点总数为 n,则有:
- n = nk + n0(总结点数 = 度为 k 的结点数 + 度为 0 的结点数)
- n = k × nk + 1(总结点数 = 所有结点的度数之和 + 1)
因此,对严格 k 叉树有 n0 = (k - 1) × nk + 1,由此可得 nk = (n0 - 1) / (k - 1) 。
- 若 (n0 - 1) % (k - 1) = 0(% 为取余运算),则说明这 n0 个叶结点(初始归并段)正好可以构造 k 叉归并树。此时,内结点有 nk 个。
- 若 (n0 - 1) % (k - 1) = u != 0,则说明对于这 n0 个叶结点,其中有 u 个多余,不能包含在 k 叉归并树中。为构造包含所有 n0 个初始归并段的 k 叉归并树,应在原有 m 个内结点的基础上再增加 1 个内结点。它在归并树中代替了一个叶结点的位置,被代替的叶结点加上刚才多出的 u 个叶结点,即再加上 k - u - 1 个空归并段,就可以建立归并树。
2. 示例
下图左侧为 3 路平衡归并的归并树,右侧为 3 路平衡归并的最佳归并树
3. 例题
① 最佳归并树在外部排序中的作用是( B )。
A. 完成 m 路归并排序
B. 设计 m 路归并排序的优化方案
C. 产生初始归并段
D. 与锦标赛树的作用类似
② 在由 m 个初始归并段构建的 k 阶最佳归并树中,不需要补充虚段,则度为 k 的结点个数是( C)。
A. (m - 1) / k
B. m / k
C. (m - 1) / (k - 1)
D. 无法确定
③ 【2013 统考真题】已知三叉树 T 中 6 个叶结点的权分别是 2, 3, 4, 5, 6, 7, T 的带权(外部)路径长度最小是( B )。
A. 27
B. 46
C. 54
D. 56
④ 【2019 统考真题】设外存上有 120 个初始归并段,进行 12 路归并时,为实现最佳归并,需要补充的虚段个数是( B )。
A. 1
B. 2
C. 3
D . 4
五、赢者树(Winner Tree)【拓展】
1. 基本概念
有 n 个选手的一棵嬴者树是一棵完全二叉树,它有 n 个外部结点和 n - 1 个内部结点,每个内部结点记录的是在该结点比赛的嬴者,根结点保存的是所有输入序列中的最小(或最大)元素。
为了确定一场比赛的赢者树,我们假设每个选手都有一个分数,而且有一个规则用来比较两个选手的分数以确定赢者。
- 在最小赢者树(min winner tree)中,分数小的选手获胜。
- 在最大赢者树(max winner tree)中,分数大的选手获胜。
- 在分数相等,即平局的时候,左孩子表示的选手获胜。
赢者树的一个优点在于:当一名选手的分数改变时,修改竞赛树比较容易。在一棵 n 个选手的赢者树中,当一个选手的分数发生变化时,需要修改的比赛场次介于 1 到 ⌈log2n⌉ 之间,因此,赢者树的重构需耗时 O(logn) 。此外, n 个选手的赢者树可以在 Θ(n) 时间内初始化,方法是沿着从叶子到根的方向,在内部结点进行 n - 1 场比赛。也可以采用后序遍历来初始化,每访问一个结点,就进行一场比赛。
2. 应用
1)排序
可以用一棵最小赢者树,用时 Θ(nlogn) 对 n 个元素排序。
① 首先,用 n 个元素代表 n 名选手对赢者树进行初始化。关键字决定每场比赛的结果,总冠军是关键字最小的元素。将该元素的关键字改为最大值(如∞),使它赢不了其他任何选手。
② 然后重构赢者树,以反映出该元素的关键字的变化。这时的总冠军是按序排在第二的元素。将该元素的关键字改为∞ ,再一次重构赢者树。这时的总冠军是按序排在第三的元素。以此类推,可以完成 n 个元素的排序。
③ 赢者树初始化的用时为 Θ(n) 。每次改变赢者的关键字并重构赢者树的用时为 Θ(logn),因为在从一个外部结点到根的路径上,所有的比赛需要重赛。赢者树的重构共需 n - 1 次。整个排序过程的时间 Θ(n + nlogn) = Θ(nlogn) 。
2)生成初始归并段
内部排序法(internal sorting method)要求待排序的元素全部放入计算机内存。但是,当待排序的元素所需要的空间超出内存的容量时,内部排序法就需要频繁地访问外部存储介质(如磁盘),那里存储着部分或全部待排的元素。这使得排序效率大打折扣。于是我们需要引入外部排序法(external sorting method)。外部排序一般包括两个步骤:
① 生成一些初始归并段,每一个初始归并段都是有序集;
② 将这些初始归并段合并为一个归并段。
在归并段合并中,决定时间的因素之一是在步骤 ① 中生成的初始归并段的个数。使用赢者树可以减少初始归并段的个数。假设一棵赢者树有 p 名选手,其中每个选手是输入集合的一个元素,它有一个关键字和一个归并段号。初始时这 p 个元素的归并段号均为 1 。当两个选手进行比赛时,归并段号小的选手获胜。在归并段号相同时,关键字小的选手获胜。
为生成初始归并段,重复地将总冠军 W 移到它的归并号所对应的归并段,并用下一个输入元素 N 取代 W。
- 如果 N 的关键字大于等于 W 的关键字,则令元素 N 的归并段号与 W 的相同,因为在 W 之后把 N 输出到同一个归并段不会影响归并段的次序。
- 如果 N 的关键字小于 W 的关键字,则令元素 N 的归并段号为 W 的归并段号加 1,因为在 W 之后把 N 输出同一个归并段将破坏归并段的排序。
当采用上述方法生成初始归并段时,初始归并段的平均长度约为 2 × p 。
3)k路合并
合并 k 个归并段的简单方法是:从 k 个归并段的前面,不断把关键字最小的元素移到正在生成的输出归并段。当所有元素从 k 个输入归并段移至输出归并段时,合并过程就完成了。
在 k 路合并中,k 个归并段合并成一个归并段。按照上述方法,每一个元素合并到输出归并段所需时间为 O(k),因为每一次迭代都需要在 k 个关键字中找到最小值。因此,产生一个大小为 n 的归并段所需要的总时间为 O(k × n) 。
而使用赢者树可将这个时间缩短为 Θ(k + nlogk)。首先用 Θ(k) 的时间初始化一棵有 k 个选手的赢者树,这 k 个选手分别是 k 个归并段的头元素。然后将赢者移入输出归并段,并从相应的输入归并段中取出下一个元素替代赢者的位置。若该输入段无下一个元素,则用一个关键字值很大(不妨为∞)的元素替代。这个提取和替代赢家的过程需要 n 次,一次需要时间为 Θ(logk) 。一次 k 路合并的总时间为 Θ(k + nlogk) 。
4)赢者树与败者树
在许多应用中,只有在一个新选手替代了前一个赢者之后,才执行重新组织比赛的操作。这时,在从赢者的外部结点到根结点的路径上,所有比赛都要重新进行。如果每个内部结点记录的是在该结点比赛的输者而不是赢者,那么当赢者改变后,在从该结点到根的路径上,重新确定每一场比赛的选手所需要的操作量就可以减少。
当一个赢者发生变化时,使用败者树可以简化重赛的过程。但是,当其他选手发生改变时,就不是那么回事了。因此,仅当发现变化的选手为前次比赛的赢家时,对于重新组织比赛的操作,采用败者树比采用赢者树执行效率更高。
赢者树:保留胜利者,便于快速提取当前最优元素,并在合并时保持高效。败者树:保留被淘汰者,在更新时具有一定的灵活性。