前面讨论的排序算法,都假定要排序的所有数据在内存中都同时可用,如数组。要对存储在外部文件中的数据排序,首先要将数据送入内存,然后对它们进行内部排序。然而,如果文件太大,那么文件中的所有数据不能都同时送入内存。在大型外部文件中对数据排序,称为外部排序(external sort)。
创建大文件
创建一个200万int值存储在一个名为largedata.dat的二进制文件中。使用下面的程序创建:
1 | import java.io.*; |
归并排序实现
实现阶段1
重复将数据从文件读入数组,并使用内部排序算法堆数组排序,然后将数据从数组输出到一个临时文件中。
下面的代码给出了一个方法,它从文件中读取每个数据段,并对分段进行排序,然后将排好序的分段存在一个心文件中。该方法返回分段的个数。
1 | private static int initializeSegments |
MAX_ARRAY_SIZE,数组的最大尺寸依赖于操作系统分配给JVM的内存大小。
假定数组的最大尺寸为100 000个int值,那么在临时文件中就是对每100 000个int值进行的排序。将它们标记为S1,S2,…,Sk,最后一段包含的数值可能会少于100 000个。
实现阶段2
将每对有序分段(比如S1,S2,…,Sk)归并到一个大一些的有序分段中,并将新分段存储到新的临时文件中。继续同样的过程直到得到仅仅一个有序分段。
每步归并都将两个有序分段归并成一个新分段。新段的元素数目是原来的两倍,因此,每次归并后分段的个数减少一半。
如果一个分段太大,它将不能放到内存的数组中。为了实现归并步骤,要将文件f1.dat中的一半数目的分段复制到临时文件f2.dat中。然后,将f1.dat中剩下的收割分段与f2.dat中的首个分段归并到名为f3.dat的临时文件中。
复制前半部分的分段
1 | private static void copyHalfToF2(int numberOfSegments, int segmentSize, |
归并所有分段
1 | private static void mergeSegments(int numberOfSegments, |
归并两个阶段
1 | private static void mergeTwoSegments(int segmentSize, DataInputStream f1, |
结合两个阶段
完整代码
1 | package sorting; |
外部排序复杂度
在外部排序中,主要开销是在IO上。假设n是文件中要排序的元素个数。在阶段1,从原始文件读取元素个数n,然后将它输出给一个临时文件。因此,阶段1的IO复杂度为O(n)。
对于阶段2,在第一个合并步骤之前,排好序的分段的个数为 n/c,其中c是MAX_ARRAY_SIZE。每一个合并步骤都会使分段的个数减半。因此,在第一次合并步骤之后,分段个数为 n/2c。
在第二次合并步骤之后,分段个数为 n/4c。 在第三次合并步骤之后,分段个数为 n/8c。
在第log(n/c)次合并步骤之后,分段个数减到1。因此,合并步骤的总数为log(n/c)。
在每次合并步骤中,从文件f1读取一半数量的分段,然后将它们写入到一个临时文件f2。合并f1中剩余的分段和f2中的分段。每一个合并步骤中IO的次数为O(n)。因为合并步骤的总数是log(n/c),IO的总数是 O(n) * log(n/c) = O(nlogn)。
因此外部排序的复杂度是O(nlogn)