外部排序
内存有限,不能一次将数据全部导入内存排序,于是有了外部排序。
例如,一个含有2000个记录的文件,每个磁盘可容纳250个记录,则该文件包含8个磁盘块。
1. 先用内部排序方法,每次读一个磁盘块,排完序后写到文件,共需8次读和8次写,生成8个有序文件。
2. 然后作二路归并排序,每次往内存读入两个磁盘块,进行归并,一旦输出缓冲区满了就写到磁盘上,第一轮下来,读写16次,有了4个有序文件;然后进行第二轮,读写16次,2个有序文件;最后再读写16次,1个有序文件,结束。
一共进行了4次外存读写(1次内排,3轮内部归并的读写)
外排总时间=内部排序总时间+外存信息读写时间+内部归并时间
tES=r∗tIS+d∗tIO+S∗(n−1)∗tmg
从上面式子看出,如果要减少总时间,就得使得IO次数尽可能少,即归并路数增加,但如果归并路数m太多,会导致内部归并效率下降(每次比较m-1次选一个最小的数,对于总记录数为n,每轮比较(n-1)*(m-1)次,共log_m(r)轮,当n和r固定时,(m-1)/log_2(m)随着m增加而增加,说了那么多,主要就是让这个选最小值的比较次数想办法减少,因为每次仅换了一个最小数,但是下一次还是要m-1次)
堆
自然而然的我们想象到了用堆来实现归并,只有第一次建立堆要线性次的复杂度,后面每次调整只有O(log_n)次,就比原来的线性复杂度小了。
胜者树
但是人们发现堆每次取出最小值之后,把最后一个数放到堆顶,调整堆的时候,每次都要选出父节点的两个孩子节点的最小值,然后再用孩子节点的最小值和父节点进行比较,所以每调整一层需要比较两次。 这时人们想能否简化比较过程,这时就有了胜者树。
这样每次比较只用跟自己的兄弟节点进行比较就好,所以用胜者树可以比堆少一半的比较次数。 而胜者树在节点上升的时候首先需要获得兄弟节点,要判断兄弟节点是叶子节点还是中间节点(因为取值方式不一样),这就又增加点复杂性,这时人们又想能否再次减少比较次数,并且只和中间节点比较,于是就有了败者树。
败者树:
两量相比较,父亲节点存储了两个节点比较的败者(节点较大的值);胜利者(较小者)可以参与更高层的比赛(因为中间节点保存的是败者,所以需要另外用S指针指向当前的胜者,最后根节点的值就是s指针指向的值)。这样树的顶端就是当次比较的冠军(最小者)。
在使用败者树的时候,每个新元素上升时,只需要获得父节点并比较即可, 所以总的来说,更加方便了。
无论是堆、胜者树、败者树,它们的时间复杂度都是同个数量级的,都比原来要好,为O(log_m),这样,内部归并的总共比较次数为:
⌈logmr⌉×(n−1)×⌈log2m⌉=(n−1)×⌈log2r⌉
表示 n条记录分r段进行m路归并时的比较次数,所以,增大归并路数不会影响内部归并的时间了(内存允许时,不然的话,随着归并路数的增加,缓冲区个数也得增加,但如果内存缓存区大小小了,就会导致IO次数增加…)
归并趟数是 ⌈log_”m” r⌉ 所以,上面通过增大m来减小归并趟数,也可以通过减小r来减小归并趟数。
前面也说过,内存大小是有限的,那么初始块如果是要用内存排序的话,最多也不会超过内存上限,那么,有没有其他排序方法能生成更大的初始块呢?
置换-选择排序
置换-选择排序可以生成大概两倍于内存大小的顺串。
基本思路:
用败者树从已经传递到内存中的记录中找到关键值最小(或最大)的记录MINIMAX,然后将此记录写入外存,再将外存中一个没有排序过的记录传递到内存(因为之前那个记录写入外存后已经给它空出内存),然后再用败者树的一次调整过程找到最小关键值记录(这个调整过程中需要注意:比已经写入本初始归并段的记录关键值小的记录不能参与筛选,它要等到本初始段结束,下一个初始段中才可以进行筛选),再将此最小关键值记录调出,再调入新的记录,如果当前的所有值都小于MINIMAX了,就开启一个新的初始段…….依此进行直到所有记录已经排序过。
好了,现在设想这样一种情况,归并段长度分别为100,10,10,工作区单位为1,如果两路归并,先100和第一个10归并,IO220次,生成110长的块,再和10归并,IO240次,一共460次,而这里面100很大一部分都是“怎么来内存怎么回内存”的,白白进了内存,多浪费啊,那么,我们自然而然想到,每次归并大小相近的块是不是更好?
这和什么很像?
Huffman树!
但用于归并,它变成了“多叉Huffman树”,太长?
其实它有个专有的名字叫最佳归并树。
最佳归并树
m-路归并排序可用一棵m叉树描述,因为每一次作m路归并都需要m个归并段参加,因为,归并段树是一棵只有度为0和度为m的结点的严格m叉树。
若初始归并段数不是m的整数倍怎么办?
正确的做法是,若初始归并段不足构成一棵严格m叉树时,需添加长度为0的“虚段”,按照Huffman树的原则,权为0的叶子应离根最远。
如何判定添加虚段的数目?
设度为0的结点有
N0(=n)
个,度为
m
的结点有
- 如果(N0-1)%(m-1)=0,则说明这N0个叶结点(初始归并段)正好可以构造m叉归并树。不用添加。
- 如果(N0-1)%(m-1)=u不等于0,则为了能整除,还需m-u-1个。