外部排序

了解外部排序的思想即可,可能在选择题中考察一题。

外部排序

当待排序的数据量非常大,以至于无法一次性全部加载到内存中时,就需要使用外部排序。

外部排序通常分为两个主要阶段:

  • 初始归并段生成: 使用置换选择排序,根据原始无序文件生成若干个尽量长的的有序文件,这些有序文件叫做初始归并段。
  • 多路归并: 将这些初始归并段逐步合并,最终得到完全有序的文件。
置换归并排序:
生成初始归并段
多路归并排序

置换选择排序

置换选择排序(Replacement Selection Sort)是外部排序的一个步骤,用于生成初始归并段, 其核心思想是在内存缓冲区有限的情况下,尽可能地生成较长的初始归并段。

置换选择排序通过维护一个工作区来实现这一目标,工作区是一个小根堆。

其算法步骤如下:

  1. 初始化:
    • 将待排序文件中的前 M 个记录读入工作区,建立一个最小堆(M 为工作区的大小)。
  2. 生成归并段:
    • 判断是否有初始归并段
      • 如果不存在任何初始归并段的话,创建一个初始归并段,并添加工作区中的最小元素加入其中。
      • 否则将工作区中的元素与上一个初始归并段的最大值 MAXV 进行比较。
        • 如果工作区中的所有元素都 ≤ MAXV 的话,创建一个新的初始归并段,并添加工作区中的最小元素加入其中。
        • 否则找到第一个 > MAV 的元素,将其添加到上一个初始归并段的末尾。
    • 从输入文件中读取下一个值加入工作区。
输入文件 IF
工作区(小根堆)
输出文件 OF1
输出文件 OF2
如果工作区中存在值
> OF1 最大值
否则创建一个
新文件

举一个实际的例子,假设一个输入文件 FI 的内容为 51, 94, 37, 92, 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100。

我们可以通过置换选择排序生成 3 个初始归并段,分别为 {37, 51, 63, 92, 94, 99} ,{14, 15, 23, 31, 48, 56, 60, 90, 166} ,{8, 17, 43, 100} 。

算法执行的过程如下表所示:

输出文件 FO工作区 WA输入文件 FI
————51, 94, 37, 92, 14, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
——51, 94, 37, 9214, 63, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
3751, 94, 14, 9263, 15, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 5163, 94, 14, 9215, 99, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 6315, 94, 14, 9299, 48, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 63, 9215, 94, 14, 9948, 56, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 63, 92, 9415, 48, 14, 9956, 23, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 63, 92, 94, 9915, 48, 14, 5623, 60, 31, 17, 43, 8, 90, 166, 100
37, 51, 63, 92, 94, 99#15, 48, 14, 5623, 60, 31, 17, 43, 8, 90, 166, 100
1415, 48, 23, 5660, 31, 17, 43, 8, 90, 166, 100
14, 1560, 48, 23, 5631, 17, 43, 8, 90, 166, 100
14, 15, 2360, 48, 31, 5617, 43, 8, 90, 166, 100
14, 15, 23, 3160, 48, 17, 5643, 8, 90, 166, 100
14, 15, 23, 31, 4860, 43, 17, 568, 90, 166, 100
14, 15, 23, 31, 48, 5660, 43, 17, 890, 166, 100
14, 15, 23, 31, 48, 56, 6090, 43, 17, 8166, 100
14, 15, 23, 31, 48, 56, 60, 90166, 43, 17, 8100
14, 15, 23, 31, 48, 56, 60, 90, 166100, 43, 17, 8——
14, 15, 23, 31, 48, 56, 60, 90, 166#100, 43, 17, 8——
8100, 43, 17——
8, 17100, 43——
8, 17, 43100——
8, 17, 43, 100————
8, 17, 43, 100#————

多路归并

多路归并的目标是将多个已经排序的初始归并段(子文件)合并成一个更大的有序文件。

其核心思想是从若干归并段中每次选取一个最小的元素加入工作区(小根堆),然后从工作区中选取最小的元素添加到输出文件中,通过这种方式可以保证每次添加到输出文件中的值是当前的最小值。

多路归并的具体过程如下:

  1. 初始化:
    • 打开所有参与归并的初始归并段,并读取每个归并段的第一个元素。
    • 将这些元素放入一个数据结构中,以便快速找到最小值(例如,最小堆)。
  2. 归并:
    • 从数据结构中取出最小的元素,将其输出到结果文件中。
    • 找到该元素所属的归并段,并从该归并段中读取下一个元素。
    • 将新读取的元素放入数据结构中,并调整数据结构,以保持有序。
    • 重复上述步骤,直到所有归并段都被处理完毕。
  3. 结束:
    • 当所有归并段都为空时,归并过程结束,结果文件即为完全有序的文件。
37
51
63
92
•••
14
15
23
31
•••
8
17
43
100
•••
初始归并段0
初始归并段1
初始归并段2
8
14
37
输出文件

继续用上文中通过置换选择算法生成的三个初始归并段 {37, 51, 63, 92, 94, 99} ,{14, 15, 23, 31, 48, 56, 60, 90, 166} ,{8, 17, 43, 100} 作为例子。 对于这三个初始归并段,多路归并的过程如下(假设采用三路归并的话):

归并段1归并段2归并段3工作区输出文件
37, 51, 63, 92, 94, 9914, 15, 23, 31, 48, 56, 60, 90, 1668, 17, 43, 100
51, 63, 92, 94, 9915, 23, 31, 48, 56, 60, 90, 16617, 43, 1008, 14, 37
51, 63, 92, 94, 9915, 23, 31, 48, 56, 60, 90, 16643, 10014, 17, 378
51, 63, 92, 94, 9923, 31, 48, 56, 60, 90, 16643, 10015, 17, 378, 14
51, 63, 92, 94, 9931, 48, 56, 60, 90, 16643, 10017, 23, 378, 14, 15
51, 63, 92, 94, 9931, 48, 56, 60, 90, 16610023, 37, 438, 14, 15, 17
51, 63, 92, 94, 9948, 56, 60, 90, 16610031, 37, 438, 14, 15, 17, 23
51, 63, 92, 94, 9956, 60, 90, 16610037, 43, 488, 14, 15, 17, 23, 31
63, 92, 94, 9956, 60, 90, 16610043, 48, 518, 14, 15, 17, 23, 31, 37
63, 92, 94, 9956, 60, 90, 16648, 51, 1008, 14, 15, 17, 23, 31, 37, 43
63, 92, 94, 9960, 90, 16651, 56, 1008, 14, 15, 17, 23, 31, 37, 43, 48
92, 94, 9960, 90, 16656, 63, 1008, 14, 15, 17, 23, 31, 37, 43, 48, 51
92, 94, 9990, 16660, 63, 1008, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56
92, 94, 9916663, 90, 1008, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60
94, 9916690, 92, 1008, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60, 63
94, 9992, 100, 1668, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60, 63, 90
9994, 100, 1668, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60, 63, 90, 92
99, 100, 1668, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60, 63, 90, 92, 94
8, 14, 15, 17, 23, 31, 37, 43, 48, 51, 56, 60, 63, 90, 92, 94, 99, 100, 166

胜者树败者树

在进行 k 路归并的过程中,我们需要从 k 个有序文件的当前值中筛选出来一个最小值加入输出文件中。 这里最简单的方式就是一次遍历,当然也可以使用胜者树,如下图所示,两者的时间复杂度都为 O(k)。

8
9
8
9
15
8
17
10
9
20
15
8
9
90
17
15
16
20
38
20
30
25
28
15
50
11
16
95
99
18
20

简而言之,胜者树(winner tree)就是通过树形结构将最小值不断向根结点传递。

败者树与胜者树不同,它在每次比较中将较大的元素(败者)向上传递并保存在中间结点中,胜者则继续与其他胜者进行下一轮的比较。最终的全局胜者(最小值)不会存储在树中,而是单独保存在树外或根节点位置,供快速访问。

9
15
17
10
20
9
90
10
9
20
15
8
9
90
17
15
16
20
38
20
30
25
28
15
50
11
16
95
99
18
20
8
9
15
9
8
17
8
8
胜者需要进行下一轮比较

对于胜者树败者树,了解两者的基本概念即可。具体来说,两者对比如下:

特性胜者树(Winner Tree)败者树(Loser Tree)
根节点存储最小值(或最大值)存储最后一轮的败者,最小值保存在树外
非叶子节点存储每轮胜者存储每轮败者
查找最小值在根节点需要从树外变量中取(一次性)
更新操作成本$O(log_2{k}))$$O(log_2{k})$,但更新路径较短
编码复杂度较简单略微复杂,但也容易实现
局部更新性能平均需要更新整条路径只需更新路径中涉及的节点,不需全树重建