External Sorting

Published: by Creative Commons Licence

外部排序,指待排序文件较大,内存依次放不下,需存放在外存的文件的排序。

在许多应用中,经常需要对大文件进行排序,因为文件中的记录很多、信息量庞大,无法将整个文件复制进内存中进行排序。因此,需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。

外部排序的方法

外部排序过程中的时间代价主要考虑访问磁盘的次数,即I/O次数

外部排序通常采用归并排序法。它包括两个相对独立的阶段:

  1. 根据内存缓冲区的大小,将外存上的文件分成若干长度为 l 的子文件,依次读入内存并利用内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写回外存,称这些有序子文件为归并段顺串
  2. 对这些归并段进行逐趟归并,并使归并段(有序子文件)逐渐由小到大,直至得到整个有序文件为止。

例如,一个含有2000个记录的文件,每个磁盘块可容纳125个记录,首先通过8次内部排序得到8个初始归并段R1~R8(每个归并段都有两个磁盘),每个段都含有250个记录。然后对该文件作两两归并,直至得到一个有序文件。

关于缓冲区的操作:

  • 首先,从两个输入归并段 R1 和 R2 中分别读入一个块,放在输入缓冲区1和输入缓冲区2中。
  • 然后,在内存中进行2路归并,归并后的对象顺序存放在输出缓冲区中。
  • 若输出缓冲区中对象存满,则将其顺序写到输出归并段(R1')中,再清空输出缓冲区,继续存放归并后的对象。
  • 若某个输入缓冲区对象取空,则从对应的输入归并段中再读取下一块,继续参加归并。
  • 如此继续,直到两个输入归并段中的对象全部读入内存并都归并完成为止。
  • 当 R1 和 R2 归并完后,再归并 R3 和 R4 …

在外部排序中实现两两归并时,由于不可能将两个有序及归并结果段同时存放在内存中,因此需要不停地将数据读出、写入磁盘,而这会耗费大量的时间。一般情况下:

外部排序的总时间 = 内部排序所需的时间 + 外存信息读写的时间 + 内部归并所需的时间

显然,外存信息读写的时间远大于内部排序内部归并的时间,因此应该着力减少I/O次数

增大归并路数,可以减少归并趟数,进而减少磁盘I/O次数

一般地,对r个初始归并段,做k路平衡归并,归并树可用严格k叉树(即只有度为0与度为k的结点的k叉树)来表示。 树的高度为ceil(log_{k}{r}),也是归并趟数S

可见,只要增大归并路数k,或者减少初始归并段个数r,都能减少归并趟数S,进而减少读写磁盘的次数,从而达到提高外部排序速度的目的

多路平衡归并与败者树

多路平衡归并时,增加归并路数k能减少归并趟数S,进而减少I/O次数。但是在增加归并路数k的时候,内部归并的时间会增加。

内部归并时,在k个元素中选择关键字最小的记录k-1次。每趟归并n个元素需要做(n-1)(k-1)次比较,S趟归并总共需要比较的次数

S(n-1)(k-2) = ceil(log_{k}{r})(n-1)(k-1) = ceil(log_{2}{r})(n-1)(k-1) / ceil(log_{2}{k})

可以发现,在上式中,(k-1)/ceil(log_{2}{k})随k增长而增长,这会抵消由于增大k而减少外存访问次数所得到的收益,所以我们不能使用普通的内部归并算法。于是我们使用败者树来实现内部归并算法。

下面是一个例子:

大的为失败者,小的为胜利者

败者树

k路归并的败者树深度为ceil(log_{2}{k}),因此k个记录中选择最小关键词,最多需要ceil(log_{2}{k})次比较。所以总的比较次数为

S(n-1)ceil(log_{2}{k}) = ceil(log_{k}{r})(n-1)ceil(log_{2}{k}) = (n-1)ceil(log_{2}{r}),

使用败者树之后,内部归并的比较次数与k无关。因此只要内存空间允许,增大归并路数k将有效减少归并树的高度,从而减少I/O次数,提高外部排序的速度。

但是,归并路数k并不是越大越好,归并路数k增大时,相应地需要增加输入缓冲区的个数。若可供使用的内存空间不变,势必要减少每个输入缓冲区的容量,时的内存、外存减缓数据的次数增大。当k过大时,虽然归并趟数会减少,但读写外存的次数仍然会增加。

置换 - 选择排序

从之前的内容中,可以知道,减少初始归并段个数r也可以减少归并趟数S。

置换 - 选择算法:产生更长的初始归并段。

置换 - 选择算法步骤:

  1. 从FI输入w个记录到工作区WA;
  2. 从WA中选出其中关键词取最小值的记录,记为MINIMAX记录;
  3. 将MINIMAX记录输出到FO中;
  4. 若FI不空,则从FI输入下一个记录到WA中;
  5. 从WA中所有关键词比MINIMAX记录的关键词大的记录中选出最小关键词记录,作为新的MINIMAX记录;
  6. 重复3~5,直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志符到FO中;
  7. 重复2~6,直至WA为空。由此得到全部初始归并段。

置换选择算法

最佳归并树

置换 - 选择算法之后,得到的是长度不等的初始归并段。下面我们将介绍如何组织长度不等的初始归并段的归并顺序,使得I/O次数最少。

正确做法:若初始归并段不足以构成一颗严格k叉树,需添加长度为0的"虚段",按照哈夫曼的原则,权为0的叶子应离树根最远。

如何判定添加虚段的数目

设度为0的结点有n0(=n)个,度为k的结点有nk个,则对严格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叉归并树,应在原有nk个内结点的基础上再增加一个内结点。他在归并树中代替了一个叶结点的位置,被替代的叶结点加上刚才多出的u个叶结点(即再加上k-u-1个空归并段),就可以建立归并树。