外部排序算法
一、核心概念:为什么需要外部排序?
外部排序是一种用于处理海量数据的排序算法,这些数据量大到无法一次性全部加载到计算机的内存(RAM) 中。
- 内存:速度快,但容量小。
- 外部存储器(如硬盘、磁带):速度慢,但容量大。
当我们需要排序的文件(比如几十GB的日志文件)远远超过内存容量(比如16GB)时,像快速排序、归并排序这样的内部排序算法就无能为力了,因为它们假设所有数据都在内存中。这时,就必须使用外部排序。
外部排序的基本思想是: 将大数据文件分块读入内存,用高效的内部排序算法对这些块进行排序,将排序后的块(称为“归并段”或“顺串”)写回磁盘。然后,再将这些有序的归并段合并成一个完整的有序文件。
这个过程的主要时间消耗不在于CPU计算,而在于磁盘I/O(输入/输出)。因此,外部排序算法的设计核心就是如何减少磁盘的读写次数。
二、外部排序的经典算法:多路归并排序
最经典的外部排序算法是基于归并排序思想的“多路归并排序”。其过程主要分为两个阶段:
阶段一:生成初始归并段
- 读取:从大文件中读取尽可能多的数据到内存中。
- 排序:在内存中使用高效的排序算法(如快速排序、堆排序)对这部分数据进行排序。
- 输出:将排序好的数据作为一个“归并段”或“顺串”写入到一个临时磁盘文件中。
- 重复:重复步骤1-3,直到将原始大文件的所有数据都处理成多个有序的归并段。
假设:我们有一个10GB的文件,内存只有1GB。
结果:我们将得到10个初始归并段,每个1GB,且每个归并段内部都是有序的。
阶段二:多路归并
这个阶段的目标是将上一步生成的多个有序归并段合并成一个完整的有序文件。
- 最简单的二路归并:每次从两个归并段中取出当前最小的元素,比较后输出最终结果。这需要多次读写。
- 多路归并:为了高效,我们使用K路归并,即同时合并K个归并段。
K路归并的过程:
- 打开K个归并段文件。
- 为每个归并段在内存中建立一个输入缓冲区,并读入一部分数据。
- 从K个归并段的当前元素中选出最小的一个(这个过程需要一个高效的数据结构,通常是败者树或最小堆)。
- 将这个最小元素输出到最终的有序文件中。
- 如果该元素所在的缓冲区为空,则从对应的归并段文件中再读入下一块数据。
- 重复步骤3-5,直到所有K个归并段的所有数据都处理完毕。
如果第一次归并后,生成的有序文件仍然太多,无法一次性合并,则需要进行多轮归并。
三、关键技术与优化
为了提升外部排序的效率,主要有以下几个关键技术和优化方向:
1. 多路归并
增加每次归并的路数K,可以减少归并的轮数,从而显著减少磁盘I/O次数。
- 归并轮数S与归并路数K、初始归并段数量N的关系: $ ( S = \lceil \log_K N \rceil ) $
- 例如:有100个初始归并段。
- 用二路归并(K=2):需要 $ (\lceil \log_2 100 \rceil = 7) $ 轮。
- 用十路归并(K=10):只需要 $ (\lceil \log_{10} 100 \rceil = 2) $ 轮。
2. 败者树
在K路归并中,我们需要快速地从K个元素中找出最小值。如果使用简单的顺序比较,每次需要K-1次比较,总时间复杂度为O(K)。当K很大时,开销很大。
败者树是一种完全二叉树,它可以在O(logK)的时间内完成每次调整,从而选出最小元。它是多路归并高效实现的核心数据结构。
3. 置换-选择排序
在生成初始归并段阶段,传统方法受限于内存大小,归并段的长度约等于内存容量。置换-选择排序算法可以生成平均长度是内存两倍的初始归并段。这意味着初始归并段的数量会更少,从而直接减少后续归并的轮数。
基本思想:在内存中维护一个最小堆。读入新记录,如果它比刚输出的记录大,就可以进入当前归并段;否则,它只能属于下一个归并段。通过这种方式,一个归并段可以“超脱”内存大小的限制。
4. 最佳归并树
如果初始归并段的长度不相等(例如使用置换-选择排序后),我们不应该简单地按顺序归并。最佳归并树借鉴了哈夫曼树的思想,将归并过程看作一棵树,长度短的段先归并,长度长的段后归并,使得总的I/O次数最小化。
四、总结
| 特性 | 内部排序 | 外部排序 |
|---|---|---|
| 数据规模 | 小,可完全放入内存 | 海量,无法放入内存 |
| 主要瓶颈 | CPU比较和交换次数 | 磁盘I/O次数 |
| 常用算法 | 快速排序、堆排序、归并排序 | 多路归并排序 |
| 核心思想 | - | 分而治之:内部排序 + 外部归并 |
| 关键技术 | - | 多路归并、败者树、置换-选择排序、最佳归并树 |
简单来说,外部排序就是通过“先局部排序,再全局合并”的策略,并运用各种优化技术来最小化耗时的磁盘读写操作,从而实现对海量数据的高效排序。

浙公网安备 33010602011771号