数据结构——外部排序

来源:互联网 发布:篮球 中华台北队 知乎 编辑:程序博客网 时间:2024/05/29 19:23

迄今为止,我们讲过的所有排序算法都需要将所有数据装入内存。但实际上存在某些情况,即需要排序的数据太多以至于无法全部装入内存,这种情况下适用的算法即是外部排序,而我们在讲归并排序时提到了,归并排序的思想是外部排序的基石


回顾一下归并排序的基础思想,很简单,两个已排好序的数组,比较它们的第一个元素,选择小的(或大的,取决于想要的顺序)放入目标数组,然后再比较各自的第一个元素,一直重复至一个数组为空,然后将另一个数组剩余元素一一放入目标数组。

 

现在转到外部排序,基础思想是一样的,就是归并。


    假设所有数据都在一个文件中,这个文件大小恰好是可用主存的两倍,那么我们可以先从文件中读取一半的数据,然后用内部排序算法将其排序,接着将这一半数据存入一个新的文件。然后我们再从文件中读取剩下的一般数据,同样在内存中将其排好序,存入一个新的文件中。最后,我们打开这两个文件,读取各自的第一个数据、比较、存入目标新文件,再读取、比较、存入……最后数据全部存入一个文件中,完成排序。

 

    在这里强调一下,在第一次读取源数据的时候(即还没开始第一次归并),每次读取我们都是读取主存能读取的最大数据量,假设为M,然后形成的单个“有序新文件”记为大小为M的“顺串”(意为已经排好顺序的一串数据)

 

    对于源数据量远大于主存的情况,外部排序的基本思想还是一样的,假设源数据有20倍主存大小,那么我们就通过读入、排序、输出生成20个顺串,然后将这20个顺串两两归并形成新的10个顺串(大小为20顺串的2倍),然后再归并,形成5个新顺串(大小为20顺串的4倍)。不断归并直到最后只剩下一个“顺串”即最终排好序的结果。

 

 

    在《数据结构与算法分析——C语言描述》中,还有两个小节:多路合并、多相合并。其中多路合并主要目的为减少合并趟数,多相合并则是为了减少使用的磁带个数(如果我们使用的不是磁带而是硬盘之类的存储介质,那么估计这个想法用处并不大,磁带的不便之处在于从前往后读取数据后如果要重头读取则需要将磁带转至开头位置,这个地方花费时间很多)

 

    多路合并的思想很简单,如果我们有16个顺串大小的数据,那么两两合并的2路合并过程将是:168421个顺串,但我们如果使用例如4路合并,那么过程就会是:1641个顺串,这将减少合并的“趟数”。特别的,如果k路合并的k值较大,可以使用堆来完成“选择”k个文件头的最小值(将k个文件头数据形成一个堆,通过DeleteMin得到最小的那个,然后从根数据所在文件再读取一个Insert

 

    多相合并可以说是基于磁带和多路合并而产生的想法,因为如果存储介质是磁带,那么进行简单的k路合并将需要2k个磁带,但是稍加努力可以避免这样大的开销,可以将磁带个数从2k降到k+1个。

因为前面简介外部排序时已经没有基于使用磁带,所以此处多相合并的内容略过。

 

 

接下来要说的是替换策略,替换策略有时候并不如标准算法,但由于很多时候数据会从近乎排序的方式开始,而这种情况下替换策略可以很好的减少趟数,所以我们在这里介绍一下这个思想。



    前面进行外部排序的想法是这样的:假设一个超大文件要排序,我们从中读取内存最大值M个数据,排序,输出一个M大小的顺串,然后再从源文件读取M个数据,一直到源文件读取完毕,形成了多个顺串,然后进行合并。



    现在替换策略是这样的:从源文件读取M个数据,建成一M大小的堆,然后DeleteMin输出一个数据到目标顺串文件中,然后从源文件读取一个数据Insert进这个堆,再DeleteMin一个,再Insert一个,以此类推……(这样一来理论上应该只生成了一个顺串,即堆没了排序就完成了)



    当然,这么简单的想法是有错的,需要改进!这个想法的问题在于:如果读取进的元素小于刚刚DeleteMin的那个元素怎么办?所以这里其实没有上面说的那么简单。那么解决方法也是有的:在我们读取的元素小于刚才DeleteMin的元素时,我们将这个新元素插入堆底(书上称死区,或许也可以通过增加一个标志域用来指示堆中的一个元素是否为死区元素),其他照旧。这样一直执行到整个堆只剩下属于死区的元素(或者美好地没有死区元素直接排序完了),此时我们结束第一个顺串文件的生成,开始生成新的顺串文件(因为死区元素不可以直接加进前一个顺串,顺序不对)……重复过程直至堆彻底空掉。最后,开始合并

0 0
原创粉丝点击