比如有这样一个基于真实场景的经典面试题:假设现在有 1TB 的任意文本,请问如何能将其中出现的单词按照字母序排列,得到一个新的文本?
这个问题的关键点其实在于,内存是有限的,而所谓外部排序的这个外:指的是外部存储。
已知1GB的内存如果要处理1TB,至少要读1024次。
所以大体思路一定是:将文件分段,用常见内部排序方法进行排序完了之后,再合并。即思路上大体是归并排序
1.内部排序
我们分成一段一段之后,主要目的是使得内存能够装下这一段数据。
一般来说,快速排序在大部分场景下都是最快的
2.归并阶段
因为需要归并n多个段,此时的内存肯定无法装下超过一个段的内容。此时最大的时间消耗来自IO。
内存的读写操作是很快的,但是外部磁盘中读写,可能比内存中要慢非常非常多。
我们看这个教程中的图片。每一次的归并,我们都得从外存中读。所以IO次数和归并层数成正比
3.如何降低归并层数——增加归并路数k
那就得每次多归并几段,上图这种每次归并两段,就是两路归并。如果我们使用k路归并,每一层都只有上一层的1/k。只需要Log Kn层即可。
4.增加k会导致什么问题
我们每次合并的成本肯定变大了,我们需要在k个元素中,每次找到最小的那个。这肯定比两个选一个代价大。
但是,很明显,选择k个元素中的最小元素,用小顶堆就能实现
而实际应用中,最广泛的方法是:
败者树败者树算法来源于体育比赛。
叶子节点两两比赛,父节点存储小的那个,这样最顶上面就是最小的元素。
和堆一样,当选择出第一个数之后,将其拿走,我们要找下一个数。
这里有两种情况:
1.正常来说我们需要找到另一个元素来替代1(即某一路元素命中,但是该路元素没有被取完)
2.取走之后不再添加新的元素(即该数是该路中最后一个元素)
不论那种情况,都需要重新比赛,只不过第二种情况,我们添加一个无限大的数,使其不可能获胜即可。
更简便的方法,我们只需要将新加入的数与原来的亚军相比较 就能知道第二次的冠军是谁。
总结回答最开始的问题:
- 先用内部排序算法,尽可能多的加载1TB文件,而后将其分成n个有序段
- 反复执行:将k个有序段合并成一个顺序段(借助败者树)。直到所有顺序段都被归并到一起。
- 注意每次从败者树中取元素,不一定产生IO,因为操作系统读取文件按页读取并缓存,大概率直接命中了cache
第一,队列长度固定
第二,插入位置在叶节点。败者树是为多路归并量身定做的,效率应该更高,堆原来为了维护满二叉树的工作在多路归并中是不需要的。
败者树、胜者树的主要区别在于内存的访问次数;胜者树是和兄弟节点做比较,所以取出父节点还要再取一次子节点;访存次数更多。败者树则解决了这个问题。而堆必然每次都需要比较两次



