我的认为,外部排序是咱们在学习过程当中接触到的一个比较重要的算法,它既包含了基本的排序算法,又考察了对文件IO以及内存的理解,还展现了最基本的程序优化思想,能够说可以写好一个外部排序,就说明基本的编程能力已通过关了。本文将对整个外部排序的过程进行详细的分析,并介绍两个经典算法,最后附上完整的程序代码。算法
1. 问题描述编程
因为在现实环境中,有时候须要对一个很是大的文件进行排序,而计算机内存是有限的,当数据没法彻底存入内存时,则没法使用正常的排序算法一次完成排序,而必须利用磁盘空间的辅助进行外部排序,即利用有限的内存每次读入部分数据排序后获得一个顺串后暂时放到磁盘,最后将多个顺串进行归并直到最终完成排序,由于在归并过程当中,只需从每一个顺串中取出最小的一个数据进行比较便可,而不须要整个顺串都在内存中,因此解决了内存空间不足的问题。那么,原问题就能够分解成两个子问题,一个是如何生成顺串,另外一个是如何将顺串进行归并。数组
首先,从性能上考虑,因为磁盘IO的速度要比内存读取的速度慢上几十万倍,因此必须尽可能减小磁盘IO次数。再考虑归并过程当中,假设有8个顺串,每次归并两个,则第一轮归并后变成4个,第二轮变成2个,直到第三轮完成归并,在这个过程当中对每一个数据进行了3次IO,而若是一次能够归并8个顺串,则只需一轮便可完成,即对每一个数据只进行了1次IO。因此,为了提升程序效率,则须要尽可能减小归并过程当中的轮数,要实现这点,能够从两个角度入手,一是减小顺串数量(即令每一个顺串的长度尽量长),二是使用多路归并,针对这两点,本文将经过选择置换算法和败者树来实现。性能
2. 选择置换学习
选择置换算法用于生成顺串,在有限的内存限制下,它能够生成大概两倍于内存大小的顺串,其算法步骤以下:优化
假设内存中只有一个能容纳N个整型的数组spa
(1)首先从输入文件中读取N个数字将数组填满 code
(2)使用数组中现有数据构建一个最小堆blog
(3)重复如下步骤直到堆的大小变为0:排序
a. 把根结点的数字A(即当前数组中的最小值)输出
b. 从输入文件中再读出一个数字B,若R比刚输出的数字A 大,则将B放到堆的根节点处,若B不比A大,则将堆的最后一个元素移到根结点,将B放到堆的最后一个位置,并把堆的大小缩减1(即新读入的数据没有进入堆中)
c. 在根结点处调用Siftdown从新维护堆
(4)换一个输出文件,从新回到步骤(2)
解释:在以上算法运行过程当中,步骤(3)每从最小堆中输出一个最小值,就从输入文件中再读入一个数据,若新读入的数比刚输出的数大,则能够属于当前的顺串,将其放入堆中便可,不然只能属于下一个顺串,需将其放在堆外,在运行过程当中,堆的大小逐渐缩减直到0,此时就输出了一个顺串,而数组中新的数则能够用于构造一个新的堆,如此循环便可将原先的一个大文件转化成一个大概2N的顺串。至于为何是2N,有一个比较抽象的类比证实:
假设在一条环形跑道上有一辆铲雪车在铲雪,且空中还在均匀地下着雪,那么当铲雪车已经沿着跑道开过一圈后,只要车速和降雪速度恒定,则跑道上的积雪量S也恒定,且车后积雪量最少,车前积雪量最多,以下图a。在这种状况下,设铲雪车每开一圈的时间,降雪量为X,车铲雪量为Y,则X,Y知足S+X - Y =S,即X = Y,又由于在铲雪车开一圈的过程当中,铲掉的雪为原有的积雪加上降雪的一半,因此Y = S + X/2, 因此Y = 2S,即铲雪车铲掉了2S的雪量。而在选择置换中,数组的大小就至关于S,铲雪量就至关于输出顺串的大小,即2倍数组大。这个证实虽然有点抽象,但实际中只要输入文件中的数字是随机分布的,获得的顺串大小的确大概是所用数组大小的两倍。
3. 败者树
在多路归并的过程当中,若是有K个顺串,每次有K个候选值,要找出其中的最小值,普通的作法须要进行K-1次比较,而使用败者树,则只须要O(logK)次比较,其原理就像咱们日常的分组比赛,一个参赛者在小组出线以后,只须要与其余小组出线的参赛者比赛便可决出最后的冠军(最值),而不须要和其余全部参赛者都比一遍。
下图为一个5路归并过程当中构建的败者树,由于要按从小到大排序,因此在每次比较中,小的为胜,大的为败。数组B[0..4]存储从顺串中读入的数,L[0]存储最终的胜者(最小值)的位置,L[1..3]存储中间各比赛败者的位置。
当前最小值为5(B[4]), 将5输出后,若新读入的数据为11,则先与该组以前的败者B[3]比较,胜后再与B[0]比较,结果为败,则将下标4记录于L[2]处,令胜者B[0]继续向上与B[2]比较,胜出后将将下标记录到L[0],通过3次比较后得出新的最小值为10(B[0]),以下图所示
关于败者树的构建和每次读入新值后的调整步骤见下面代码。
1 void CreateLoserTree(Run **runs, int n) 2 { 3 for(int i = 0; i < n; i++) 4 { 5 ls[i] = -1; 6 } 7 for(int i = n-1; i >= 0; i--) 8 { 9 Adjust(runs, n, i); 10 } 11 } 12 void Adjust(Run **runs, int n, int s) 13 { 14 //首先根据s计算出对应的ls中哪个下标 15 int t = (s + n) / 2; 16 int tmp; 17 18 while(t != 0) 19 { 20 if(s == -1) 21 break; 22 if(ls[t] == -1 || runs[s]->buffer[runs[s]->idx] > 23 runs[ls[t]]->buffer[runs[ls[t]]->idx]) 24 { 25 tmp = s; 26 s = ls[t]; 27 ls[t] = tmp; 28 } 29 t /= 2; 30 } 31 ls[0] = s; 32 }
4. 代码解释
附件中包含两个程序:
generate_data.cpp 用于生成10000000个不重复的随即数字
external_sort.cpp 用于完成外部排序
在外部排序的程序中,限制只能使用一个大小为1000000的数组做数据存储,用于生成顺串和多路归并的输入缓冲区。
程序运行结果以下图
程序源代码下载