数据结构与算法系列——排序(15)_外部排序

核心部分

1. 实现外部排序的两个过程:html

  1. 将整个初始文件分为多个初始归并段;
  2. 将初始归并段进行归并,直至获得一个有序的完整文件;

2. 时间组成:算法

  1. 内部排序所须要的时间
  2. 外存信息读写所须要的时间 (关键) 
    • 与归并的趟数有关 
      • k要大 —– 传统方法 会引发内部归并时间增大 
        • 赢者树
        • 败者树(目的:提升在k个归并串中当前值中找到最小值的效率)
      • m要小 —– 置换选择排序
    • Huffman(归并的顺序,对外存的I/O次数降到最低)
  3. 内部归并所须要的时间    

3. 为了提升整个外部排序的效率,分别从以上两个方面对外部排序进行了优化:数组

  1. 在实现将初始文件分为 m 个初始归并段时,为了尽可能减少 m 的值,采用置换-选择排序算法(内部使用败者树实现),可实现将整个初始文件分为数量较少的长度不等的初始归并段。
  2. 同时在将初始归并段归并为有序完整文件的过程当中,为了尽可能减小读写外存的次数,采用构建最佳归并树的方式(哈夫曼树实现),对初始归并段进行归并(败者树实现),而归并的具体实现方法是采用败者树的方式。

4. 优化递进顺序:性能

  1. 二路归并【由于硬盘的读写速度比内存要慢的多,按照以上这种方法,每一个数据都从硬盘读了三次,写了三次,要花不少时间。考虑K路】
  2. 多路归并【K不是越大越好,由于K越大,在内部排序须要的时间越长,效率低。考虑减小初始顺串的数量M】
  3. 置换选择算法【能够用败者树和堆排序实现,获得多个长度不等的初始归并段,如何设置它们的归并顺序,可使得对外存的访问次数降到最低? 考虑结合哈夫曼树】
  4. 最佳归并树(置换选择算法+哈夫曼树+多路归并+败者树)

5 胜者树 & 败者树 & 堆排序学习

发展历史优化

  :其实一开始就是只有堆来完成多路归并的,可是人们发现堆每次取出最小值以后,把最后一个数放到堆顶,调整堆的时候,每次都要选出父节点的两个孩子节点的最小值,而后再用孩子节点的最小值和父节点进行比较,因此每调整一层须要比较两次。 
  胜者树:这时人们想可否简化比较过程,这时就有了胜者树,这样每次比较只用跟本身的兄弟节点进行比较就好,因此用胜者树能够比堆少一半的比较次数。 而胜者树在节点上升的时候首选须要得到父节点,而后再得到兄弟节点,而后再比较。spa

  败者树:这时人们又想可否再次减小比较次数,因而就有了败者树。在使用败者树的时候,每一个新元素上升时,只须要得到父节点并比较便可。 .net

  因此总的来讲,减小了访存的时间。 如今程序的主要瓶颈在于访存了,计算倒几乎能够忽略不计了。3d

 

相同点指针

首先它们三个的相同点就是在于:空间和时间复杂度都是同样的O(N*logN)。调整一次的时间复杂度都是O(logN)的。 
因此这道题用堆来作,跟用败者树来作并无本质上的算法复杂度量级上的差异。

 

不一样点

  堆:全部的节点都是关键字; 每次调整一层须要比较两次(父亲 左孩子|  父亲 右孩子)。 
  胜者树:叶子节点是关键字,非叶子节点保存胜者索引;每次调整一层须要比较1次(本身 兄弟),读取两次(父亲| 兄弟)。

  败者树:叶子节点是关键字,非叶子节点保存败者索引;每次调整一层须要比较1次(本身 父亲),读取一次(父亲),只须要和路径上的节点比较,不须要和兄弟节点比较,简化了重构的过程。; 新增B[0]记录比赛的胜者【在本例子中是ls[0]】

6. 涉及到的算法:

  1. 多路归并算法
  2. 败者树选择算法
  3. 置换选择算法
  4. 哈夫曼树
  5. 内部排序算法(堆排序)

1、定义

外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件没法一次装入内存,须要在内存和外部存储器之间进行屡次数据交换,以达到排序整个文件的目的。

2、步骤

  外部排序算法由两个阶段构成:预处理和合并排序。

  1. 预处理产生有序的顺串:按照内存大小,将外存上含有 n 个纪录的大文件分红若干长度为 t 的子文件(t 应小于内存的可以使用容量),而后将各个子文件依次读入内存,使用适当的内部排序算法对子文件的 t 个纪录进行排序(排好序的子文件统称为“归并段”或者“顺段”),将排好序的归并段从新写入外存,为下一个子文件排序腾出内存空间;这样在外存上就获得了m个顺串(m=[n/t])。
  2. 合并序列:对获得的顺段进行合并,直至获得整个有序的文件为止。

3、k-路平衡归并

 

3.1 2-路平衡归并

例子1

  给你一个包含20亿个int类型整数的文件,计算机的内存只有2GB,怎么给它们排序?一个int数占4个字节,20个亿须要80亿字节,大概占用8GB的内存,而计算机只有2GB的内存,数据都装不下!能够把8GB分割成4个2GB的数据来排,而后在把他们拼凑回去。以下图:

   

         在2G内存中排序的时候能够选择合适的内部排序,好比快速排序或归并排序等算法。为了方便,咱们把排序好的2G有序数据称为有序子串。接着把两个小的有序子串合并成一个大的有序子串。

   

         注意:读取的时候是每次读取一个int数,经过比较以后再输出。按照这个方法来回合并,总共通过三次合并以后就能够获得8G的有序子串。

         咱们假设须要排序的int数有12个,内存一次只能装下3个int数。

   

         接下来把12个数据分红4份,而后排序成有序子串:

   

         而后把子串进行两两合并:

   

         输出哪一个元素就在那个元素所在的有序子串再次读入一个元素:

   

         继续

   

         重复直到合并成一个包含6个int有序子串:

   

         再把两个包含6个int的有序子串合并成一个包含12个int数据的最终有序子串:

  

由于硬盘的读写速度比内存要慢的多,按照以上这种方法,每一个数据都从硬盘读了三次,写了三次,要花不少时间。

解释下:例如对于数据2,咱们把无序的12个数据分红有序的4个子串须要读写各一次,把2份3个有序子串合并成6个有序子串读写各一次;把2份6个有序子串合并从12个有序子串读写各一次,一共须要读写各3次。

在进行有序子串合并的时候,不采起两两合并的方法,而是能够3个子串,或4个子串一块儿来合并。

 

例子2

  例如,有一个含有 10000 个记录的文件,可是内存的可以使用容量仅为 1000 个记录,毫无疑问须要使用外部排序算法,具体分为两步:

  • 将整个文件其等分为 10 个临时文件(每一个文件中含有 1000 个记录),而后将这 10 个文件依次进入内存,采起适当的内存排序算法(快排或者归并排序)对其中的记录进行排序,将获得的有序文件(初始归并段)移至外存。
  • 对获得的 10 个初始归并段进行如图 1 的两两归并,直至获得一个完整的有序文件。

注意:此例中采用了将文件进行等分的操做,还有不等分的算法,后面会介绍。

  
  
                              图 1 2-路平衡归并


  如图 1 所示有 10 个初始归并段到一个有序文件,共进行了 4 次归并,每次都由 m 个归并段获得 ⌈m/2⌉ 个归并段,这种归并方式被称为 2-路平衡归并。

注意:在实际归并的过程当中,因为内存容量的限制不能知足同时将 2 个归并段所有完整的读入内存进行归并,只能不断地取 2 个归并段中的每一小部分进行归并,经过不断地读数据和向外存写数据,直至 2 个归并段完成归并变为 1 个大的有序文件。

对于外部排序算法来讲,影响总体排序效率的因素主要取决于读写外存的次数,即访问外存的次数越多,算法花费的时间就越多,效率就越低。

计算机中处理数据的为中央处理器(CPU),如若须要访问外存中的数据,只能经过将数据从外存导入内存,而后从内存中获取。同时因为内存读写速度快,外存读写速度慢的差别,更加影响了外部排序的效率。
对于同一个文件来讲,对其进行外部排序时访问外存的次数同归并的次数成正比,即归并操做的次数越多,访问外存的次数就越多

 

3.2 多路归并

对于同一个文件来讲,对其进行外部排序时访问外存的次数同归并的次数成正比,即归并操做的次数越多,访问外存的次数就越多。3.1 中图 1 中使用的是 2-路平衡归并的方式,触类旁通,还可使用 3-路归并、4-路归并甚至是 10-路归并的方式。

例子1

 咱们假设内存一共能够装4个int型数据。

         刚才咱们是采起两两合并的方式,如今咱们能够采起4个有序子串一块儿合并的方式,这样的话,每一个数据从硬盘读写的次数各须要2次就能够了。如图:

   

         4个有序子串的合并,叫4路归并。若是是n个有序子串的合并,就把它称为n路归并。n并不是越大越好。N越大,内部排序所须要的时间越多。
例子2

图 2 为 5-路归并的方式:

 
                            图 2 5-路平衡归并


对比3.1 中 图 1 和 3.2 中图 2能够看出,对于 k-路平衡归并中 k 值得选择,增长 k 能够减小归并的次数,从而减小外存读写的次数,最终达到提升算法效率的目的。除此以外,通常状况下对于具备 m 个初始归并段进行 k-路平衡归并时,归并的次数为:s=⌊logk⁡m ⌋(其中 s 表示归并次数)。

从公式上能够判断出,想要达到减小归并次数从而提升算法效率的目的,能够从两个角度实现:

  • 增长 k-路平衡归并中的 k 值;[但不能影响内部归并的效率]
  • 尽可能减小初始归并段的数量 m,即增长每一个归并段的容量;

其增长 k 值的想法引伸出了一种外部排序算法:多路平衡归并算法;增长数量 m 的想法引伸出了另外一种外部排序算法:置换-选择排序算法。

 

4、多路平衡归并排序(胜者树、败者树)算法

  对于外部排序算法来讲,其直接影响算法效率的因素为读写外存的次数,即次数越多,算法效率越低。若想提升算法的效率,即减小算法运行过程当中读写外存的次数,能够增长 k –路平衡归并中的 k 值。可是通过计算得知,若是毫无限度地增长 k 值,虽然会减小读写外存数据的次数,但会增长内部归并的时间,得不偿失。(k越大,内部归并排序【好比选出最小值】须要花费更多的时间,因此k不是越大越好)
  例如在上节中,对于 10 个临时文件,当采用 2-路平衡归并时,若每次从 2 个文件中想获得一个最小值时只需比较 1 次;而采用 5-路平衡归并时,若每次从 5 个文件中想获得一个最小值就须要比较 4 次。以上仅仅是获得一个最小值记录,如要获得整个临时文件,其耗费的时间就会相差很大。
  为了不在增长 k 值的过程当中影响内部归并的效率,在进行 k-路归并时可使用“败者树”来实现,该方法在增长 k 值时不会影响其内部归并的效率。

   胜者树和败者树都是彻底二叉树(非叶子节点存储的是索引),他们是树形选择排序的变形(非叶子节点存储的是具体的值)。

4.1 胜者树实现内部归并

咱们对胜者树进行定义:
  1. 胜者树是一颗彻底二叉树
  2. 胜者树的叶子结点保存咱们的一个输入缓冲区(一路归并顺序表);  叶节点L[ 1……n]
  3. 胜者树的非叶子节点保存当前比较的胜者的输入缓冲区的指针;非叶子节点B[1……n-1] //存储的是数组L的索引
  4. 胜者树的根节点保存咱们的胜者树当前的的一次比较中的冠军(最优值) B[0]

    

  当咱们将咱们的胜者树的最优值输入到咱们的输出缓冲区(输出缓冲区从内存中额外开辟出来的一段,咱们存储当前的归并的结果,缓冲区满写入磁盘
以后,咱们的根节点便出现了空的状况,咱们须要从根节点对应的输入缓冲区中在读入一个数据来充当下一次比较的选手,而后从下到上进行维护咱们的每一次的维护都须要比较兄弟的胜者而后选出新一轮的胜者而后一直优化到咱们的根的路径上(从低至上,贯穿整个树)以后咱们不断地进行上述的操做,指导咱们的全部的输入缓冲区已经为空为止。

 

例子:

胜者树的一个优势是,若是一个选手的值改变了,能够很容易地修改这棵胜者树。只须要沿着从该结点到根结点的路径修改这棵二叉树,而没必要改变其余比赛的结果。

咱们把胜者树分为两部分:
b[]:用来保存K路数组的首元素,叶节点存放在此处,即底下那七个数组
ls[]:用来保存胜者数组的下标,ls[1]是最终的胜者(即所求的数)。


胜者树的中间结点记录的是胜者的标号
胜者树的示例。规定数值小者胜。

这里写图片描述
b3 PK b4,b3胜b4负,内部结点ls[4]的值为3;
b3 PK b0,b3胜b0负,内部结点ls[2]的值为3;
b1 PK b2,b1胜b2负,内部结点ls[3]的值为1;
b3 PK b1,b3胜b1负,内部结点ls[1]的值为3。

叶子结点b3的值变为11时,重构的胜者树如图所示
这里写图片描述 
1. b3 PK b4,b3胜b4负,内部结点ls[4]的值为3;
2. b3 PK b0,b0胜b3负,内部结点ls[2]的值为0;
3. b1 PK b2,b1胜b2负,内部结点ls[3]的值为1;
4. b0 PK b1,b1胜b0负,内部结点ls[1]的值为1。.

 

4.2 败者树实现内部归并

  咱们的胜者树维护的时候每次都须要去查找咱们的根的兄弟节点的位置来进行比较,可是咱们的每一次都要多一步查找兄弟的划,不管是对咱们的程序的实现过程仍是咱们的时间效率上来看都还存在改进的余地。这里咱们就要引入败者树,败者树与胜者树刚好相反,其双亲结点存储的是左右孩子比较以后的失败者,而胜利者则继续同其它的胜者去比较。


败者树的定义:

  1. 败者树是一颗彻底二叉树(败者树是树形选择排序的一种变形)
  2. 败者树的叶子结点保存的是咱们的输入缓冲区
  3. 败者树的非叶子结点保存咱们的当前的比较中败者的对应的输入缓冲区的指针
  4. 败者树根保存咱们的当前比较的亚军,根上面还有一个节点保存咱们的冠军

比胜过程

  1. 将新入树的节点与其父亲进行比较
  2. 把败者存放在父亲节点
  3. 把胜者再与上一级的父亲进行比赛
  4. 比赛不断进行
  5. 把败者的索引存放在B[1]
  6. 把胜者的索引放到节点B[0]

例子1:

  败者树是胜者树的一种变体。在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,须要加一个结点来记录整个比赛的胜利者。采用败者树能够简化重构的过程。

咱们把败者树分为两部分:
b[]:用来保存K路数组的首元素,叶节点存放在此处,即底下那七个数组
ls[]:用来保存败者数组的下标,b[0]是最终的胜者(即所求的数),败者节点存放在中间节点。
败者树的中间结点记录的败者的标号
败者树示例,规定数大者败。

这里写图片描述
b3 PK b4,b3胜b4负,内部结点ls[4]的值为4;
b3 PK b0,b3胜b0负,内部结点ls[2]的值为0;
b1 PK b2,b1胜b2负,内部结点ls[3]的值为2;
b3 PK b1,b3胜b1负,内部结点ls[1]的值为1;
在根结点ls[1]上又加了一个结点ls[0]=3,记录的最后的胜者。

 

败者树重构过程以下:
将新进入选择树的结点与其父结点进行比赛:将败者存放在父结点中;而胜者再与上一级的父结点比较。
比赛沿着到根结点的路径不断进行,直到ls[1]处。把败者存放在结点ls[1]中,胜者存放在ls[0]中。
是当b3变为13时,败者树的重构图:

这里写图片描述
  注意,败者树的重构跟胜者树是不同的,败者树的重构只须要与其父结点比较。b3与结点ls[4]的原值比较,ls[4]中存放的原值是结点4,即b3与b4比较,b3负b4胜,则修改ls[4]的值为结点3。同理,以此类推,沿着根结点不断比赛,直至结束。

例子2:

  例如仍是图 1 中,叶子结点 49 和 38 比较,38 更小,因此 38 是胜利者,49 为失败者,但因为是败者树,因此其双亲结点存储的应该是 49;一样,叶子结点 65 和 97 比较,其双亲结点中存储的是 97 ,而 65 则用来同 38 进行比较,65 会存储到 97 和 49 的双亲结点的位置,38 继续作后续的胜者比较,依次类推。

胜者树和败者树的区别就是:胜者树中的非终端结点中存储的是胜利的一方;而败者树中的非终端结点存储的是失败的一方。而在比较过程当中,都是拿胜者去比较。

  
            图 2 败者树
 

  如图 2 所示为一棵 5-路归并的败者树,其中 b0—b4 为树的叶子结点,分别为 5 个归并段中存储的记录的关键字。ls 为一维数组,表示的是非终端结点,其中存储的数值表示第几归并段(例如 b0 为第 0 个归并段)。ls[0] 中存储的为最终的胜者,表示当前第 3 归并段中的关键字最小。

  当最终胜者判断完成后,只须要更新叶子结点 b3 的值,即导入关键字 15,而后让该结点不断同其双亲结点所表示的关键字进行比较,败者留在双亲结点中,胜者继续向上比较。

  例如,叶子结点 15 先同其双亲结点 ls[4] 中表示的 b4 中的 12 进行比较,12 为胜利者,则 ls[4] 改成失败者 15 所在的归并段即 b3,而后 12 继续同 ls[2] 中表示的 10 作比较,10 为胜者,则 ls[2] 改成失败者 12 所在的归并段即 b4,而后 10 继续同其双亲结点 ls[1] 表示的 b1(关键字 9)做比较,最终 9 为胜者。整个过程以下图所示:


 

注意:为了防止在归并过程当中某个归并段变为空,处理的办法为:能够在每一个归并段最后附加一个关键字为最大值的记录。这样当某一时刻选出的冠军为最大值时,代表 5 个归并段已所有归并完成。(由于只要还有记录,最终的胜者就不多是附加的最大值)

   本节介绍了经过使用败者树来实现增长 k-路归并的规模来提升外部排序的总体效率。可是对于 k 值得选择也并非一味地越大越好,而是须要综合考虑选择一个合适的 k 值。

4.3 胜者树 & 败者树 & 堆排序

发展历史

  :其实一开始就是只有堆来完成多路归并的,可是人们发现堆每次取出最小值以后,把最后一个数放到堆顶,调整堆的时候,每次都要选出父节点的两个孩子节点的最小值,而后再用孩子节点的最小值和父节点进行比较,因此每调整一层须要比较两次。 
  胜者树:这时人们想可否简化比较过程,这时就有了胜者树,这样每次比较只用跟本身的兄弟节点进行比较就好,因此用胜者树能够比堆少一半的比较次数。 而胜者树在节点上升的时候首选须要得到父节点,而后再得到兄弟节点,而后再比较。

  败者树:这时人们又想可否再次减小比较次数,因而就有了败者树。在使用败者树的时候,每一个新元素上升时,只须要得到父节点并比较便可。 

  因此总的来讲,减小了访存的时间。 如今程序的主要瓶颈在于访存了,计算倒几乎能够忽略不计了。

相同点

首先它们三个的相同点就是在于:空间和时间复杂度都是同样的O(N*logN)。调整一次的时间复杂度都是O(logN)的。
因此这道题用堆来作,跟用败者树来作并无本质上的算法复杂度量级上的差异。

不一样点

  堆:全部的节点都是关键字; 每次调整一层须要比较两次(父亲 左孩子|  父亲 右孩子)。 
  胜者树:叶子节点是关键字,非叶子节点保存胜者索引;每次调整一层须要比较1次(本身 兄弟),读取两次(父亲| 兄弟)。

  败者树:叶子节点是关键字,非叶子节点保存败者索引;每次调整一层须要比较1次(本身 父亲),读取一次(父亲),只须要和路径上的节点比较,不须要和兄弟节点比较,简化了重构的过程。; 新增B[0]记录比赛的胜者【在本例子中是ls[0]】

 

5、置换选择排序算法详解

    k 不是越大越好,那么咱们能够想办法减小有序子串的总个数 m。这样,也能减小数据从硬盘读写的次数。

  上一节介绍了增长 k-路归并排序中的 k 值来提升外部排序效率的方法,而除此以外,还有另一条路可走,即减小初始归并段的个数,也就是本章第一节中提到的减少 m 的值。

m 的求值方法为:m=⌈n/l⌉(n 表示为外部文件中的记录数,l 表示初始归并段中包含的记录数)

  若是要想减少 m 的值,在外部文件总的记录数 n 值必定的状况下,只能增长每一个归并段中所包含的记录数 l。而对于初始归并段的造成,就不能再采用上一章所介绍的内部排序的算法,由于全部的内部排序算法正常运行的前提是全部的记录都存在于内存中,而内存的可以使用空间是必定的,若是增长 l 的值,内存是盛不下的。因此要另想它法,探索一种新的排序方法:置换—选择排序算法。



  例如已知初始文件中总共有 24 个记录,假设内存工做区最多可容纳 6 个记录,按照以前的选择排序算法最少也只能分为 4 个初始归并段。而若是使用置换—选择排序,能够实现将 24 个记录分为 3 个初始归并段,如图 1 所示:


  
              图 1 选择排序算法的比较


  置换—选择排序算法的具体操做过程为:

    1. 首先从初始文件中输入 6 个记录到内存工做区中;
    2. 从内存工做区中选出关键字最小的记录,将其记为 MINIMAX 记录;
    3. 而后将 MINIMAX 记录输出到归并段文件中;
    4. 此时内存工做区中还剩余 5 个记录,若初始文件不为空,则从初始文件中输入下一个记录到内存工做区中;
    5. 从内存工做区中的全部比 MINIMAX 值大的记录中选出值最小的关键字的记录,做为新的 MINIMAX 记录;[使用败者树或者堆排序实现]
    6. 重复过程 3—5,直至在内存工做区中选不出新的 MINIMAX 记录为止,由此就获得了一个初始归并段;
    7. 重复 2—6,直至内存工做为空,由此就能够获得所有的初始归并段。


  拿图 1 中的初始文件为例,首先输入前 6 个记录到内存工做区,其中关键字最小的为 29,因此选其为 MINIMAX 记录,同时将其输出到归并段文件中,以下图所示:

  

  此时初始文件不为空,因此从中输入下一个记录 14 到内存工做区中,而后从内存工做区中的比 29 大的记录中,选择一个最小值做为新的 MINIMAX 值输出到 归并段文件中,以下图所示:

  

  初始文件还不为空,因此继续输入 61 到内存工做区中,从内存工做区中的全部关键字比 38 大的记录中,选择一个最小值做为新的 MINIMAX 值输出到归并段文件中,以下图所示:

  

  如此重复性进行,直至选不出 MINIMAX 值为止,以下图所示:

  

  当选不出 MINIMAX 值时,表示一个归并段已经生成,则开始下一个归并段的建立,建立过程同第一个归并段同样,这里再也不赘述。

 

5.1 堆排序实现

咱们要如何从内存中选出这个目的数呢?难道每次都把内存中的数据进行排序,而后再逐个比较选择吗?其实咱们能够构建一个最小堆来帮助咱们选择目的数。具体以下:

   12个无序的数

  

         从12个数据中读取3个数据,构建一个最小堆,而后从堆顶选择一个数写入到p1中。以后再从剩余的9个数中读取一个数,若是这个数比刚才那个写入到p1中的数大,则把这个数插入到最小堆中,从新调整最小堆结构,而后在堆顶选一个数写入到p1中。不然,把这个数暂放在一边,暂时不处理。以后同样须要调整堆结构,从堆顶选择一个数写入到p1中。这里说明一下,那个被放在一边的数是不能在放入p1中的了,由于它必定比p1中的数都要小,因此它会放在下一个子串中。以下图所示:

         从12个数据中读取3个数据:

  

         构建最小堆,且选出目标数:

   

         读入下一个数86:

 

         读入下一个数3,比70小,暂放一边,不加入堆结构中:

 

         读入下一个数据24,比81小,不加入堆结构:

 

         读入下一个数据8,比86小,不加入堆结构。此时p1已经完成了,把那些刚才暂放一边的数从新构成一个堆,继续p2的存放:

 

         以此类推…最后生成的p2以下:

 

         这样子的话,最后只生成了2个有序子串,咱们把这种方法称之为置换选择。按照这种方法,最好的状况下,全部数据只生成一个有序子串;最坏的状况下,和原来没采起置换选择算法同样,仍是4个子串,那平均性能如何呢?

         结论:若是内存能够容纳n个元素的话,那么平均每一个子串的长度为2m,也就是说,使用置换选择算法咱们能够减小一半的子串数。

 

5.2 败者树实现

  在上述建立初始段文件的过程当中,须要不断地在内存工做区中选择新的 MINIMAX 记录,即选择不小于旧的 MINIMAX 记录的最小值,此过程须要利用“败者树”来实现。
  同上一节所用到的败者树不一样的是,在不断选择新的 MINIMAX 记录时,为了防止新加入的关键字值小的的影响,每一个叶子结点附加一个序号位,当进行关键字的比较时,先比较序号,序号小的为胜者;序号相同的关键字值小的为胜者。
  在初期建立败者树时也能够经过不断调整败者树的方式,其中全部记录的序号均设为 0 ,而后从初始文件中逐个输入记录到内存工做区中,自下而上调整败者树。过程以下:

    1. 首先建立一个空的败者树,以下图所示:

      提示:败者树根结点上方的方框内表示的为最终的胜者所处的位置。

    2. 从初始文件中读入关键字为 51 的记录,自下往上调整败者树,以下图所示:

      提示:序号 1 为败者。 先比较序号,序号小的为胜者;序号相同的关键字值小的为胜者。

    3. 从初始文件中读入关键字为 49 的记录,调整败者树以下图所示:


       
    4. 从初始文件依次读入关键字为 3九、4六、3八、29 的记录,调整败者树以下图所示:


  由败者树得知,其最终胜者为 29,设为 MINIMAX 值,将其输出到初始归并文件中,同时再读入下一个记录 14,调整败者树,以下图所示:


  

  注意:当读入新的记录时,若是其值比 MINIMAX 大,其序号则仍为 1;反之则为 2 ,比较时先比较序号,序号小的为胜者;序号相同的关键字值小的为胜者。。

  经过不断地向败者树中读入记录,会产生多个 MINIMAX,直到最终全部叶子结点中的序号都为 2,此时产生的新的 MINIMAX 值的序号 2,代表此归并段生成完成,而此新的 MINIMAX 值就是下一个归并段中的第一个记录。

 

 

经过置换选择排序算法获得的初始归并段,其长度并不会受内存容量的限制,且经过证实得知使用该方法所得到的归并段的平均长度为内存工做区大小的两倍。

经过对初始文件进行置换选择排序可以得到多个长度不等的初始归并段

证实此结论的方法是 E.F.Moore(人名)在 1961 年从置换—选择排序和扫雪机的类比中得出的,有兴趣的能够本身了解一下。

若不计输入输出的时间,经过置换选择排序生成初始归并段的所需时间为O(nlogw)(其中 n 为记录数,w 为内存工做区的大小)

 

6、最佳归并树详解

 

6.1 哈夫曼树

  经过上一节对置换-选择排序算法的学习了解到,经过对初始文件进行置换选择排序可以得到多个长度不等的初始归并段,相比于按照内存容量大小对初始文件进行等分,大大减小了初始归并段的数量,从而提升了外部排序的总体效率。

  本节带领你们思考一个问题:不管是经过等分仍是置换-选择排序获得的归并段,如何设置它们的归并顺序,可使得对外存的访问次数降到最低
  例如,现有经过置换选择排序算法所获得的 9 个初始归并段,其长度分别为:9,30,12,18,3,17,2,6,24。在对其采用 3-路平衡归并的方式时可能出现如图 1 所示的状况:


  
            图 1 3-路平衡归并

提示:图 1 中的叶子结点表示初始归并段,各自包含记录的长度用结点的权重来表示;非终端结点表示归并后的临时文件。

  假设在进行平衡归并时,操做每一个记录都须要单独进行一次对外存的读写,那么图 1 中的归并过程须要对外存进行读或者写的次数为:(9+30+12+18+3+17+2+6+24)*2*2=484(图 1 中涉及到了两次归并,对外存的读和写各进行 2 次)从计算结果上看,对于图 1 中的 3 叉树来说,其操做外存的次数刚好是树的带权路径长度的 2 倍。因此,对于如何减小访问外存的次数的问题,就等同于考虑如何使 k-路归并所构成的 k 叉树的带权路径长度最短。若想使树的带权路径长度最短,就是构造赫夫曼树。

在学习赫夫曼树时,只是涉及到了带权路径长度最短的二叉树为赫夫曼树,其实扩展到通常状况,对于 k 叉树,只要其带权路径长度最短,亦能够称为赫夫曼树。

  若对上述 9 个初始归并段构造一棵赫夫曼树做为归并树,如图 2 所示:


  
        图 2 赫夫曼树做为3-路归并树


依照图 2 所示,其对外存的读写次数为:(2*3+3*3+6*3+9*2+12*2+17*2+18*2+24*2+30)*2=446

经过以构建赫夫曼树的方式构建归并树,使其对读写外存的次数降至最低(k-路平衡归并,须要选取合适的 k 值,构建赫夫曼树做为归并树)。因此称此归并树为最佳归并树。

 

6.2 附加“虚段”的归并树

上述图 2 中所构建的为一颗真正的 3叉树(树中各结点的度不是 3 就是 0),而若 9 个初始归并段改成 8 个,在作 3-路平衡归并的时候就须要有一个结点的度为 2。
对于具体设置哪一个结点的度为 2,为了使总的带权路径长度最短,正确的选择方法是:附加一个权值为 0 的结点(称为“虚段”),而后再构建赫夫曼树。例如图 2 中若去掉权值为 30 的结点,其附加虚段的最佳归并树如图 3 所示:


          图 3 附加虚段的最佳归并树

注意:虚段的设置只是为了方便构建赫夫曼树,在构建完成后虚段自动去掉便可。

对于如何判断是否须要增长虚段,以及增长多少虚段的问题,有如下结论直接套用便可:在通常状况下,对于 k–路平衡归并来讲,若 (m-1) MOD (k-1) = 0,则不须要增长虚段;不然需附加 k - (m-1)MOD(k-1) - 1 个虚段。 

7、代码

 

8、总结

1. 实现外部排序的两个过程

  1. 将整个初始文件分为多个初始归并段;
  2. 将初始归并段进行归并,直至获得一个有序的完整文件;

2. 时间组成

  1. 内部排序所须要的时间
  2. 外存信息读写所须要的时间 (关键) 
    • 与归并的趟数有关 
      • k要大 —– 传统方法 会引发内部归并时间增大 
        • 赢者树
        • 败者树
      • m要小 —– 置换选择排序
    • Huffman(归并的顺序)
  3. 内部归并所须要的时间    

3.外部排序优化

为了提升整个外部排序的效率,分别从以上两个方面对外部排序进行了优化:

  1. 在实现将初始文件分为 m 个初始归并段时,为了尽可能减少 m 的值,采用置换-选择排序算法(内部使用败者树或者堆排序实现),可实现将整个初始文件分为数量较少的长度不等的初始归并段。
  2. 同时在将初始归并段归并为有序完整文件的过程当中,为了尽可能减小读写外存的次数,采用构建最佳归并树的方式(哈夫曼树实现)对初始归并段进行归并(败者树实现),而归并的具体实现方法是采用败者树的方式。

4. 优化递进顺序

  1. 二路归并【由于硬盘的读写速度比内存要慢的多,按照以上这种方法,每一个数据都从硬盘读了三次,写了三次,要花不少时间。考虑K路】
  2. 多路归并【K不是越大越好,由于K越大,在内部排序须要的时间越长,效率低。考虑减小初始顺串的数量M】
  3. 置换选择算法【能够用败者树和堆排序实现,获得多个长度不等的初始归并段,如何设置它们的归并顺序,可使得对外存的访问次数降到最低? 考虑结合哈夫曼树】
  4. 最佳归并树(置换选择算法+哈夫曼树+多路归并+败者树)

5 胜者树 & 败者树 & 堆排序

发展历史

  :其实一开始就是只有堆来完成多路归并的,可是人们发现堆每次取出最小值以后,把最后一个数放到堆顶,调整堆的时候,每次都要选出父节点的两个孩子节点的最小值,而后再用孩子节点的最小值和父节点进行比较,因此每调整一层须要比较两次。 
  胜者树:这时人们想可否简化比较过程,这时就有了胜者树,这样每次比较只用跟本身的兄弟节点进行比较就好,因此用胜者树能够比堆少一半的比较次数。 而胜者树在节点上升的时候首选须要得到父节点,而后再得到兄弟节点,而后再比较。

  败者树:这时人们又想可否再次减小比较次数,因而就有了败者树。在使用败者树的时候,每一个新元素上升时,只须要得到父节点并比较便可。 

  因此总的来讲,减小了访存的时间。 如今程序的主要瓶颈在于访存了,计算倒几乎能够忽略不计了。

相同点

首先它们三个的相同点就是在于:空间和时间复杂度都是同样的O(N*logN)。调整一次的时间复杂度都是O(logN)的。 
因此这道题用堆来作,跟用败者树来作并无本质上的算法复杂度量级上的差异。

不一样点

  堆:全部的节点都是关键字; 每次调整一层须要比较两次(父亲 左孩子|  父亲 右孩子)。 
  胜者树:叶子节点是关键字,非叶子节点保存胜者索引;每次调整一层须要比较1次(本身 兄弟),读取两次(父亲| 兄弟)。

  败者树:叶子节点是关键字,非叶子节点保存败者索引;每次调整一层须要比较1次(本身 父亲),读取一次(父亲),只须要和路径上的节点比较,不须要和兄弟节点比较,简化了重构的过程。; 新增B[0]记录比赛的胜者【在本例子中是ls[0]】

6. 涉及到的算法

  1. 多路归并算法
  2. 败者树选择算法
  3. 置换选择算法
  4. 哈夫曼树
  5. 内部排序算法(堆排序)

摘录网址

  1. 【漫画】什么是外部排序?
  2.  什么是外部排序算法
  3.  多路平衡归并排序(胜者树、败者树)算法
  4. 置换选择排序算法
  5. 最佳归并树
  6. 置换-选择排序总结
  7. 外部排序
  8. 胜者树 败者树 K-路最佳归并树 高效外部排序
  9. 外排序-置换选择排序,赢者树,败者树
  10. 外部排序优化之败者树与胜者树
  11. 堆,赢者树,败者树的区别与联系
相关文章
相关标签/搜索