零、核心部分
1. 实现外部排序的两个过程:
- 将整个初始文件分为多个初始归并段;
- 将初始归并段进行归并,直至得到一个有序的完整文件;
2. 时间组成:
- 内部排序所需要的时间
- 外存信息读写所需要的时间 (关键)
- 与归并的趟数有关
- k要大 —– 传统方法 会引起内部归并时间增大
- 赢者树
- 败者树(目的:提高在k个归并串中当前值中找到最小值的效率)
- m要小 —– 置换选择排序
- k要大 —– 传统方法 会引起内部归并时间增大
- Huffman(归并的顺序,对外存的I/O次数降到最低)
- 与归并的趟数有关
- 内部归并所需要的时间
3. 为了提高整个外部排序的效率,分别从以上两个方面对外部排序进行了优化:
- 在实现将初始文件分为 m 个初始归并段时,为了尽量减小 m 的值,采用置换-选择排序算法(内部使用败者树实现),可实现将整个初始文件分为数量较少的长度不等的初始归并段。
- 同时在将初始归并段归并为有序完整文件的过程中,为了尽量减少读写外存的次数,采用构建最佳归并树的方式(哈夫曼树实现),对初始归并段进行归并(败者树实现),而归并的具体实现方法是采用败者树的方式。
4. 优化递进顺序:
- 二路归并【因为硬盘的读写速度比内存要慢的多,按照以上这种方法,每个数据都从硬盘读了三次,写了三次,要花很多时间。考虑K路】
- 多路归并【K不是越大越好,因为K越大,在内部排序需要的时间越长,效率低。考虑减少初始顺串的数量M】
- 置换选择算法【可以用败者树和堆排序实现,得到多个长度不等的初始归并段,如何设置它们的归并顺序,可以使得对外存的访问次数降到最低? 考虑结合哈夫曼树】
- 最佳归并树(置换选择算法+哈夫曼树+多路归并+败者树)
5 胜者树 & 败者树 & 堆排序
发展历史
堆:其实一开始就是只有堆来完成多路归并的,但是人们发现堆每次取出最小值之后,把最后一个数放到堆顶,调整堆的时候,每次都要选出父节点的两个孩子节点的最小值,然后再用孩子节点的最小值和父节点进行比较,所以每调整一层需要比较两次。
胜者树:这时人们想能否简化比较过程,这时就有了胜者树,这样每次比较只用跟自己的兄弟节点进行比较就好,所以用胜者树可以比堆少一半的比较次数。 而胜者树在节点上升的时候首选需要获得父节点,然后再获得兄弟节点,然后再比较。
败者树:这时人们又想能否再次减少比较次数,于是就有了败者树。在使用败者树的时候,每个新元素上升时,只需要获得父节点并比较即可。
所以总的来说,减少了访存的时间。 现在程序的主要瓶颈在于访存了,计算倒几乎可以忽略不计了。
相同点
首先它们三个的相同点就是在于:空间和时间复杂度都是一样的O(N*logN)。调整一次的时间复杂度都是O(logN)的。
所以这道题用堆来做,跟用败者树来做并没有本质上的算法复杂度量级上的差别。
不同点
堆:所有的节点都是关键字; 每次调整一层需要比较两次(父亲 左孩子| 父亲 右孩子)。
胜者树:叶子节点是关键字,非叶子节点保存胜者索引;每次调整一层需要比较1次(自己 兄弟),读取两次(父亲| 兄弟)。
败者树:叶子节点是关键字,非叶子节点保存败者索引;每次调整一层需要比较1次(自己 父亲),读取一次(父亲),只需要和路径上的节点比较,不需要和兄弟节点比较,简化了重构的过程。; 新增B[0]记录比赛的胜者【在本例子中是ls[0]】
6. 涉及到的算法:
- 多路归并算法
- 败者树选择算法
- 置换选择算法
- 哈夫曼树
- 内部排序算法(堆排序)
一、概述
外排序是指数据存放在外存中,数据排序时涉及内、外存数据交换的排序方法。
存储在外存上的数据以文件为基本单位,由文件系统进行读写操作,读写操作的基本单位为物理块。
外排序的基本方法是归并排序法:
一、生成若干初始归并段(顺串):这一过程也称为文件预处理。一种常规的方法如下:
- 把含有n个记录的文件,按内存大小w分成若干长度为w的子文件(归并段);
- 分别将各子文件(归并段)调入内存,采用有效的内排序方法排序后送回外存。
- 产生m=[n/w]个初始归并段。
此时产生的若干子文件称为顺串。
二、多路归并:
(所谓多路归并,具体使用几路归并是看计算机内存大小的,如计算机内存为2则只能使用2路归并)
对这些初始归并段进行多遍归并,使得有序的归并段逐渐扩大,最后在外存上形成整个文件的单一归并段,也就完成了这个文件的外排序。
例子
1、将一个大文件分成M个小文件,每个小文件是有序的
2、然后我们从M个子文件中各取一个数字进入内存处理(将该数字打上队列编号标记方便后续处理)
3、比较中转站中的数据,当从中转站出来的最小数字(升序排列)就是我们最后要排序的数字之一,将其写入到归并结果文件
因为该数字打上了队列编号,所以方便我们通知对应的编号队列继续出数字进入中转站队列(保证从中转站中取出的数据是所有文件中最小的),可以看出中转站一直保存了M个记录,当中转站中的所有数字都出队完毕,则本次归并结束。
因为每个小文件都是有序的顺串,所以当内存取出所有小文件第一个数据处理后,将从输出的那个数据所属小文件中再次拿出一个数据,保证了内存中的数据都是各个小文件中最小的那一个。
多次使用归并算法,直到所有小文件都被处理完(以二路归并为例):
总结
1、u个记录需要进行u-1次操作(对各个顺串中数据进行比较)(不考虑∞ (文件中数据读取完再取的话设为∞))。
2、k路归并每次操作需要k-1次关键字比较。
3、总的关键字比较次数为:(u-1)(k-1)。
4、影响归并过程性能因素:
○ 记录读写次数。
○ 内存中归并时需要关键字比较次数。
○ 外排序方法与各种外存设备的特征有关。
影响内排序的数据位置交换,在外排序中变成了记录读写次数,所以此时影响外排序方法性能的主要原因为外存存取数据所花费的时间,即外排序方法与各种外存设备的特征有关。
外存设备大体上可以分为两类:
1.顺序存取设备(磁带)
2.直接存取设备(磁盘)
二、磁盘排序
磁盘是直接存取设备,读取一个数据块的时间与当前读写头所处位置关系不大,所以可以通过读写数据块的次数来衡量存取时间。
对存放在磁盘中的文件进行排序属于典型的外排序,称为磁盘排序(disk sort),其磁盘排序过程与外排序所用归并排序过程基本相同。
磁盘排序过程
磁盘中的Fin文件存储着待排序的数据,通过计算讲Fin文件中的记录一部分一部分的调入内存处理,产生若干个文件F1…Fm,他们经过处理后都是有序的,称为顺串
,然后再次将F1…Fm依次调入内存,通过相关归并算法生成Fout文件 。
实际上文件系统在读取时每次读取一个物理块,而一个物理块中可能有多个记录(数据),但为了方便起见以下每个记录占用一个物理块(读写记录只需要进行一次读写操作)
1.生成初始顺串
方法1(常规方法)
按照内存大小将初始文件分为若干份( 我们假设需要排序的int数有12个,内存一次只能装下3个int数,就把12个数据分成4份(12/3=4),然后通过内排序处理成有序子串)
- 根据给定文件记录数n,内存空间w,有初始归并个数m=[n/w](取上界)
- 内存中排序的时候可以选择快速排序或归并排序等算法。
方法2:置换-选择排序方法
置换-选择排序方法生成初始归并段,该方法可以减少生成的初始归并段个数(初始归并段个数越少,则越接近最后排序结果,可以减少归并次数)
- 从待排文件Fin中按内存工作区WA的容量w读入w个记录。设归并段编号i=1。
- 从WA中选出关键字最小的记录Rmin。
- 将Rmin记录输出到当前归并段Fi。
- 若Fin不空,则从Fin中读入下一个记录x放在Rmin所在的工作区位置代替Rmin。
- 在工作区中所有≥Rmin的记录中选择出最小记录作为新的Rmin,重复(3),直到选不出这样的Rmin。
- 设i=i+1,开始一个新的归并段。
- 若工作区已空,则初始归并段已全部产生;否则执行(2)。
总结:
1、共有n个记录,内存工作区WA的容量为w:
2、若在w个记录中选取最小关键字的采用简单比较方法,每次需要w-1次比较。
使用败者树可以提高比较效率。
3、总的时间复杂度为O(nw)。
读出和写入均为n。
2.处理顺串形成有序文件
1、多路平衡归并
什么是k路平衡归并:
根据内存大小可以划分为不同的多路归并方案
2路平衡归并:每一趟从m个归并段得到[m/2]个归并段。
处理时把两个小的有序子串合并成一个大的有序子串(二路归并)
多次使用归并算法,直到所有小文件都被处理完
总结:
每归并一次,参与归并的每个记录都要读一次和写一次。
总的读记录数(写记录数与之相同):
(F1+F2+F3+F4的记录数)×2=24=对起始12条记录读(写)了2遍=log2m
该数越大,效率越差
当m不满足2的i次方时,增加两个长度为0的虚段
总的读记录数等同于哈夫曼树WPL(带权路径长度):
- 从根结点到各个叶结点的路径长度与相应结点权值的乘积的和
- 显然不同的归并方案所需要的读写记录数是不同的(使用二路归并或归并方案不同的二路归并、三路归并造成的读写次数也不同)
影响k路平衡归并的因素
归并时需要读写磁盘的次数
k路平衡归并时读写磁盘次数的计算:m=8,假设每个归并段4个记录:k=2
读记录次数 = WPL = 8×4×3 = 96(如果每个记录占用一个物理块,读写磁盘次数=96×2=192次)
采用k路平衡归并时,通常k越大,读写磁盘次数会减少。归并时需要关键字比较的次数
k路平衡归并时关键字比较次数的计算
采用k路平衡归并时,则相应的归并树有[logkm]+1层,要对数据进行[logkm]趟扫描。
总共需要的关键字比较次数为:
增大归并路数k,读写磁盘次数减少,而关键字比较次数会增大。若k增大到一定的程度,就会抵消掉由于减少读写磁盘次数而赢得的时间。
2.利用败者树实现k路平衡归并过程
败者树
败者树是一棵有k个叶子节点的完全二叉树(可以将大根堆看成胜者树,小根堆看成败者树),其中叶子节点存储参与归并的记录,分支节点存放关键字对应的段号。
所谓败者
是两个记录比较时关键字较大者,胜者是两个记录比较时关键字较小者
败者树用于在k个记录中选取最小关键字的记录。败者树类似于堆排序中的小根堆。
败者树是一棵有k个叶子节点的完全二叉树(可以将大根堆看成胜者树,小根堆看成败者树),其中叶子节点存储参与归并的记录,分支节点存放关键字对应的段号。
利用败者数实现k路平衡归并的过程,
是先建立败者树,然后对k个输入有序段进行k路平衡归并
一、先建立败者树
建立败者数是采用类似于小根堆调整的方法实现的,初始时令所有的分支结点指向一个含最小关键字的叶子节点,然后从叶子结点出发,调整分支节点为新的败者(比较时关键字较大者)即可
- 计算树中结点个数:
败者树是一棵有k个叶子节点的完全二叉树(结点个数最少),由二叉树性质,n2=n0-1=k-1,n=n0+n1+n2=2k-1+n1,为了让其结点最少,则让n1=0,n=2k-1(其中n为败者树中结点,下标为该结点的度数),另外添加一个冠军结点(存放数中最小者)。
例子,k=5:
首先构造非叶子节点n2=n0-1=k-1=4
每个叶子结点对应一个归并段,段号为0~4。
初始时每个分支结点取值“5(-∞)”,5表示段号(此时为虚拟段号,实际段号为0~4),- ∞表示最小关键字。
例如,某结点取值为“4(15)”,表示结点值来自4号段的关键字15对应的记录。
调整产生冠军(最小者)的过程
从F4=>F0重复操作:将当前结点的关键字与父结点比较,将大的(败者)放在父结点中,小者(胜者)继续进行,直到根结点。最后将胜者放在冠军结点中。
二、用败者树进行归并
1、取每个输入有序段的第一个记录,作为败者树的叶子节点,建立初始败者数
两两叶子节点进行比较,在双亲节点中存放比较的败者(关键字较大者),而让胜者去参加更高一层的比赛,如此在根节点之上胜出的冠军是所有关键字最小者
2、将胜出的记录(冠军)写至输出归并段,在对应的叶子节点处补充其输入有序段的下一个记录,若该有序段变空,则补充一个大关键字(比所有关键字都大,设为∞)的虚记录 ∞
3、调整败者树,选择新的冠军(最小的记录)
从补充记录的叶子节点向上和双亲结点的关键字比较败者留在该双亲节点,胜者继续向上,直到树的根结点,最后将胜者放在根节点的双亲节点中 作为冠军
4、若胜出的记录关键字等于∞则归并结束,否则重复(2)
例子,归并过程:
取出的冠军为1(5),从1号段中取下一个记录,沿着个分支向上操作,产生次小的记录。重复操作直到冠军为∞
总结
利用败者树实现k路平衡归并时,总共需要的关键字比较次数为:
关键字比较次数与k无关 , 总的内部归并时间不会随k的增大而增大。
只要内存空间允许,尽可能增大归并路数k。
- 采用败者树,置换-选择排序中关键字比较次数分析
共有n个记录,内存工作区WA的容量为w:若在w个记录中选取最小关键字的采用败者树方法,每次需要log2w次比较。总的时间复杂度为O(nlog2w)。
三、最佳归并树
使用常规方法生成的初始归并段记录数是相同的,使用置换-选择排序方法生成的顺串可能会导致顺串与顺串中记录个数不同。
k路平衡归并适合初始归并段中的记录个数相同的情况,当初始归并段中的记录个数不同时哪些初始归并段先归并,哪些后归并都会影响算法的性能。
若记录数多的先归并记录数少的归并段后归并,会导致该归并段参与的归并次数比记录少的归并段归并次数多,而读写次数计算和哈夫曼树WPL相同,从根结点到各个叶结点的路径长度与相应结点权值的乘积的和,可知算法效率显然不高 。
参考于
【外排序】外排序算法(磁盘排序、磁带排序) 外存设备结构分析 败者树多路归并 最佳归并树白话讲解_列队猫的博客-CSDN博客_外排序