大家好,数据结构与算法这一
讲,我们继续第九章,外排序的学习。今天我们学习外排序的相关算法。
外排序是对磁盘文件进行的排序操作。
它主要分为两个步骤。首先,就是形成
尽可能长的初始的排序顺串。
另外呢,我们对这些顺串最后进行一些处理,
把它整理成整个排序的文件。
那我们来看形成初始顺串的一个有效的算法。这是一个
置换选择排序。那我们用到这样的一个内部的堆。
然后我们不断地从输入缓冲区里面输出,哦,数据呢,到
堆。然后,哦,经过堆的这些处理,我们得到
输出缓冲区里面的一个有序的这样一个序列的结果。
这个结果呢我们把它输出到磁盘里面,就形成了初始的排序顺串。
那我们来看,置换选择排序,
它的处理的一个过程。那首先呢,
嗯,它,哦,形成了这么一个初始的堆。那这些堆呢,
它元素本身也是从我们待排的那个整体的大文件里面,
获得的这些数据。 然后我们这个堆顶呢,它是可以出到
我们这样的一个输出的结果里面。
哦,这个输入的数据里面,哦,这些值,它
是不是能够输出到顺串,是不是能够进入我们的堆?
那需要跟这个堆顶进行比较。如果这个堆顶呢,
它, 哦,比我们,哦,输入的这个数值要小,
那这个数值呢,可以进入到堆,我们,哦,进行后面的这个处理。
那进到堆里面呢,我们显然是要对这个堆呢,进行一个从新的调整。
那如果这个值比堆顶要小,那它就不能够进到我们这个堆里面。
哦,那这个堆呢我们把它缩小规模1。那我们
哦,把这个堆底下的那个元素换上去,哦,作为这个堆顶,
而且从新调整堆。那,哦,这个哦,
不合法的这个值,那我们还是搁在这个存储单元里面。我们等
待下一次,见下一个顺串的时候,我们可以用这个值。
那我们一直这样子处理,那我们这个堆的规模越来越小,
然后呢,我们这个输出的这个顺串呢,它当然是越来越大。
然后呢,我们这些,哦,不能够进入刚才这个堆的这些数据呢,哦,它慢慢慢慢地
就是,哦,就充满了这个被缩小的这个堆的这些空间。然后我们
在这个规模缩为,哦,0,也就是这个堆里面没有东西了以后,
它又用这些哦刚才存在这里的这些值呢,再重新建堆,
然后我们再从这个待处理的输入文件里面,去找数据,然后再去形成
另外一个这样的已排序的顺串。
那,哦,我们再来看一下,
置换选择排序它的算法。那这些部分呢,
就是这个算法的相关的一些参数的定义。 那,我们来看算法的核心的过程。
那,我们是对这个堆呢,哦,进行操作,
哦,一直到这个堆的规模还没有缩小为0的时候,
那我们,哦,首先从这个堆顶呢,去,哦,找到它相应的
一个,哦,根的这个元素。然后把这个元素 输出到,这个顺串里面。
接着呢,我们从输入缓冲区里面读一个记录,跟这个堆顶进行比较。
如果它比堆顶要大,那我们可以把它就放到,哦,这个空的堆顶。
啊,如果它比堆顶要小,我们先把这个堆的最后的那个元素呢给它
转到,哦,堆顶。然后呢,我们把这个新的,这个较小的元素呢,
给它放到我们腾出来的那个最后的位置,而堆的规模要减1。
啊然后,这两种情况下,我们都是要对这个,哦,堆
进行一个重新的调整工作,都是从根开始的。那我们一直重复
这个过程 ,那直到这个堆的规模呢,是变成了0。
然后我们形成了一个已经排序的这么一个顺串。
然后我们,就把那个数组里面拥有的那些哦,那些乱七八糟的这个元素呢,
我又重新建堆,然后再继续从待处理的这个排序文件里面,
来找新的元素来进行,形成顺串的这样的工作。
那我们看,哦,这样的一个置换选择排序,它能形成多大规模的顺串呢?
那如果你这个堆的规模你是能够容纳M个记录,那
最小的情况下,我们这个顺串长度,起码是这M个记录。就是你这个原来
最开始形成那个堆。那,最好的情况下如果你输入就是一个有序的序列,
那我们可以整个经过一轮处理,就变成整个有序的这个状态。
那平均情况下呢,我们是能够形成长度为2M的顺串。
那我们看,这么一个扫雪机的模型。 假设这是一个操场,形成一个圆圈。
然后呢,我们现在呢,雪呢已经下满这个操场,
而且呢正在均匀地下着,那在
经过了这几轮的这个清扫,达到一个稳定状态以后呢,
我们可以看到,每一轮,这样的清扫,它
所得到的这个雪的量呢,就是我刚才 存在这个操场上的这些雪的容量的两倍。
哦,因为我们再往前头看的时候,这个是一个满的。
然后我这个后面呢,是,正好是空的,那我一直这么走过来,
每扫一圈,都是扫到两倍的雪量。那,嗯,
已经形成了这么若干个初始顺串以后,
那我们怎么样最终形成一个已排序的文件呢? 那,哦, 很显然可以用归并的方法。
那因为这些原始的顺串,它的长短是不一定的,
因此我们在归并的这个过程中呢,其实是
因该用Huffman 树来规划它的这个归并顺序。
那我们来看一下这个二路归并的例子。
那我们如果是这些原始顺串,是这6个,然后每一个呢,它的
这个长度都是由3个磁盘块组成。
那每一个磁盘块里面呢有250个记录,那我们这样的两两归并去进行。
那我们可以看到呢,哦,它所需要的这么一个
磁盘的访外次数,就是读和写呢, 各有48次。
那我们,哦,如果要提高它的这样的一个归并效率呢,
我们可能需要采用多路归并,例如这就是一个3路归并的例子。
那在多路归并的情况下,我们每一次从这几个顺串里面找最小。
那如果我是,哦,逐个去看,那这是一个比较低效的方法。
而且如果k比较大的时候,我们是不是能够,哦,复用前面已经看过的这个结果呢?
那我们的方法就是采用一种叫做选择树的这样一个结构。
那我们可以达到一个更高效的一个操作的结果。
那我们看一下这个赢者树。 那这个赢者树呢,我们是这么来组织的。
那这些方框就是我们待处理的这个顺串。
那,哦,其实呢我们是把这个顺串呢,
读取了若干个这样的它的前面的几框。然后
放到这个磁盘的缓冲区,放到内存的这个缓冲区里面。
那,哦,为了组织这个比赛呢我们形成这样一个完全二叉树。
那这些圆圈的内部节点呢,它就是索引的我们在这些,哦,
外部结点它的比赛结果的那个赢者的那个顺串的索引下标。
那我们有这么几个参数。
那根据我们这个外部结点,
它是在最外层还是在倒数第二层,
我们可以得到它相应的这个父结点的下标位置。
那我们来看一下,这个,呃,赢者树,啊。
那它呢,呃,很明显就是,呃,从这个左到右,来
安排这个外部结点的编号的。 然后,内部结点呢,我们还是按照,呃,完全二叉树从上到下,从左到右来编号的。
那我们,呃,把所有的这些两两比赛
的结果的赢者,把它放在这个,呃,当前这个结点。
那,我再来,呃,看它的一个,重构的这个过程。
假设我们当前的这样的一个全局最优的是,呃,这个,顺串4,然后呢,它的值是6。 那,这个,呃,
6,它出列以后呢,那,我们后面,它的这个次小要顶向,呃,又要顶上来。
那15顶上来以后呢,它继续进行比赛。
那我们就是,要跟它的相应的这个兄弟结点进行比赛。
然后呢,我们再选出这个全局的最优。那,这一次呢,
就是,啊,这个顺串5,呃,所在的这个 顺串,然后它的值是8。
那这里边呢我们看到,在比赛的时候我还要去看这个,兄弟的结点。
那这个就不是特别方便,因此我们有一种修改的方法呢,就是败方树。
这个败方树,它的特点就是,我每次呢,把这个,
呃,失败的,这个,呃,顺串的索引, 放在我这个,呃,当前这个父结点的这个位置。
而胜者呢,我们其实是往上面去传递了。
其他的这些参数是跟赢者树是一样的。 那我们来看,嗯,
有一个,这个特殊的情况,就是, 呃,外部结点,
个数,它如果是奇数的情况下呢,我们会有一次内部节点和外部结点的比较。
那在这个时候,我们,呃,处理这些结点时候要,呃,做一下特殊的处理。
那我们,嗯,再来看,对于这样的一个败方树,
那,呃,当我们选出全局的最优者的时候,然后我,下面,
把它出列,然后次优顶上来的时候,我们比赛呢,只需要
去跟父结点,呃,那就是说,是上次比赛的那个,呃,失败的那一方,
来去比,就传到根,比刚才的赢者树,要更便捷一些。
那我们看一下,败方树的ADT。那现在是,
一些这个参数的定义,那我们,重要的是这么一个Play的函数。
还有后面这个,呃,就是,嗯, 我们建立这样的一个败方树的一个初始化的
工作。还有,在一个全局最优,出列以后,
我们来,呃,重置这个败方树的这么一个操作。
那我们,呃,来看,呃,就是初始化败方树的这样一个过程。
那,最开始呢,是一些,呃,边界的判断,以及我们计算相关的,这个参数。
然后呢,我们从底层开始比赛。那,呃,
这一次的循环,就是对这个底层的这些结点对呢,进行比赛。
那我们,呃,如果是,呃,
n为奇数的情况,就会要安排一次内部结点和外部结点的比赛。
呃,而且呢,我们这个步长的变化呢,要注意特殊处理一下。
呃,这一轮,这个,循环呢,是对剩余的外部结点,也就是,这个倒数第二层
的这些外部结点呢,进行比赛。那我们再来看一下比赛的这个函数,Play。
那,呃,我们已经知道,这个,呃,当前的这个,
左右子孩子,然后我们在,这个P结点这,我们来进行比赛。
那,我们把这个,呃,败者呢,就放在这个P结点这,这个是父结点,啊。
然后呢,我们,呃,这个胜者,先给它存到一个临时的temp里面。
然后我再看,呃,我如果是从右边来的,也就说左边都比完了,
那这个时候呢,呃,我应该是采用了这么一个败者树的一个结构。
那,它的一个临时的全局的胜者呢,它也是放在这个,祖父这个位置。
那,这个时候呢,我要,跟这个当前从右边来的,呃,
这个位置的,这个结点呢,我要去跟,这个,呃,祖父再去进行比较。
然后呢,呃,把这个败者呢,还是放在祖父这个位置。
然后胜者呢,我们要继续往上面传递,然后,我,而且,
当前这个父结点这呢,我是给它除2,就变成了祖父。
那如果是,呃,这一轮比赛都完了以后,那就是说,
我们,呃,是,从左边来的啦,这个时候我可以停止了。
我就把这个,呃,你前面比赛的这样一个局部的最优呢,
我放到祖父这个位置,那就等着后面从右边来的继续跟它比。
那我们可以看,这样的一个,呃,败者树的比赛,它是,
呃,从左到右,那两个外部结点,一对,然后一刀一刀地这样地切过来的。
那,如果切到右边的时候,它要对前面的这个结果负责。一直要比到它自己,
呃,从这个右路分支一直往上面走,走到,呃,
最左那个,就是再没有可能再往右边来的了,
才停止。那我们,呃,可以看一下这个重构的过程。
呃,重构呢,是非常简单的。我们,首先是判断这样的一个边界条件,
以及去,呃,寻找我当前需要重构的这个 i 结点, 它的,呃,父结点。
然后呢,我们,呃,就,呃,沿着这个路径往上去比赛。那这个比赛呢,
我只,那这个比赛呢,我只需要跟我那个父结点进行比较,不需要去看,
我是要看左兄弟呢,还是要看右兄弟。
那,呃,比的过程呢,也还是我们把这个,呃,失败的这一个结点呢,
我给它,呃,放到这样的一个,嗯,
祖父结点这。然后我把这个胜利的这个结点呢,我给它
干脆放在B【0】这块。那因为我们反正刚才这个B【0】全局最优已经出列了,
然后我们现在的目的就是整体地, 得到一个正确的B【0】,所以我就用B【0】作为这样一个临时量。