对1个亿数据进行排序,内存只有10M应该使用什么算法
因为数据量(1亿个数据项,即使每个数据项只是一个4字节整数,也需要约400MB的存储,这远大于10MB的可用内存)远超出了内存的容量。我们无法一次性将所有数据加载到内存中进行内部排序(例如使用快速排序、归并排序、堆排序等)。因此,需要使用外部排序算法,其中最常用和经典的是外部多路归并排序。
阶段一:生成初始有序的顺串
- 分块读取与内部排序:
- 我们将10MB的内存尽可能多地用作输入缓冲区。假设每个数据项是 S 字节,那么10MB内存大约可以容纳 M 等于 (10 乘以 1024 乘以 1024) 除以 S 个数据项。
- 我们从磁盘上读取 M 个数据项到内存中。
- 使用一种高效的内部排序算法(例如快速排序或堆排序)对这 M 个数据项进行排序。堆排序在空间上可能更优,因为它原地排序且不需要额外的递归栈空间(如果迭代实现)。
- 将排序后的这 M 个数据项(现在是一个有序的“顺串”)写回到磁盘上的一个临时文件中。
- 重复操作:
- 重复上述步骤,直到所有的1亿个数据项都被处理完毕。
- 这样,磁盘上就会生成 N 等于 (总数据量) 除以 M 个临时的、各自内部有序的顺串文件。例如,如果每个数据项是4字节整数,10MB内存约能容纳 250 万个整数。那么1亿个整数会产生 1亿 除以 250万 等于 40 个初始有序顺串。
阶段二:多路归并
现在我们有了 N 个已排序的顺串文件。我们需要将它们合并成一个最终的有序文件。
-
分配内存缓冲区:
- 10MB的内存需要被合理分配。一部分用作 K 个输入缓冲区(每个对应一个要归并的顺串),另一部分用作一个输出缓冲区。
- 假设我们为每个输入缓冲区分配 B_in 大小的空间,为输出缓冲区分配 B_out 大小的空间。那么 K 乘以 B_in 加上 B_out 的和必须小于或等于 10MB。
- K 的值(即一次归并多少个顺串)是一个关键参数。K 越大,归并的趟数就越少,总的输入输出次数也可能越少。我们希望在可用内存下尽可能增大 K。
-
执行K路归并:
- 从 K 个要归并的顺串文件中,各读取一块数据(例如 B_in 大小)到对应的输入缓冲区中。
- 使用一个最小堆数据结构,大小为 K。堆中每个元素存储的是对应输入缓冲区当前可用的最小数据项及其来源顺串的标识。
- 重复以下操作,直到所有输入顺串都处理完毕: a. 从最小堆中取出(并删除)堆顶元素(即当前所有输入缓冲区中最小的数据项)。 b. 将这个最小数据项写入输出缓冲区。 c. 如果输出缓冲区满了,就将其内容追加写入到最终的排序结果文件中,并清空输出缓冲区。 d. 从该最小数据项的原输入缓冲区中,读取下一个数据项。如果该输入缓冲区已空,则从其对应的磁盘顺串文件中读取下一块数据填充该缓冲区。如果该顺串文件已读完,则标记该路归并结束,不再向堆中添加来自此路的数据。 e. 将新读取的数据项(如果存在)插入到最小堆中。
-
多趟归并(如果需要):
- 如果初始顺串的数量 N 大于一次可以归并的路数 K(即 N 大于 K),那么第一趟归并会产生 N 除以 K 向上取整 个新的、更长的有序顺串。
- 然后对这些新生成的顺串再进行下一趟归并,直到最终只剩下一个完全排序的顺串,即最终的排序结果。
- 例如,如果有40个初始顺串,而内存允许我们进行10路归并(K等于10):
- 第一趟:40个顺串,每10个归并一次,产生 40 除以 10 等于 4 个更长的顺串。
- 第二趟:这4个顺串,进行一次4路归并(因为4小于10,这是可行的),产生最终的1个完全排序的文件。
具体到10MB内存和1亿数据的例子:
- 假设数据项是4字节整数。
- 阶段一(生成顺串):
- 10MB内存大约可以容纳 10 乘以 1024 乘以 1024 除以 4,约等于 262万 个整数。
- 我们将读取这么多整数,排序,写回磁盘。
- 总共会产生 1亿 除以 262万,约等于 38.15,即大约39个初始有序顺串。
- 阶段二(多路归并):
- 我们需要为39个输入缓冲区(每个对应一个顺串)和一个输出缓冲区分配10MB内存。
- 如果每个缓冲区至少需要能容纳一些数据(比如几KB到几十KB,以便进行有效的块输入输出),我们可以计算K的最大值。
- 假设我们给每个输入缓冲区分配100KB,输出缓冲区也分配100KB。那么 K 乘以 100KB 加上 100KB 的和应小于等于 10MB(即10240KB)。
- K 乘以 100KB 小于等于 10140KB,所以 K 小于等于 101.4。
- 这意味着我们可以同时归并多达约101路。
- 由于我们只有39个初始顺串,而39小于101,所以我们只需要一趟归并就可以完成所有39个顺串的合并,直接生成最终的排序结果文件。
- 具体地,我们会为这39个顺串各开一个输入缓冲区,再加一个输出缓冲区。10MB 除以 (39 加 1),即10MB 除以 40,等于 256KB。每个缓冲区可以有256KB,这对于磁盘输入输出来说是相当不错的块大小。
优化和考虑点:
- 替换选择算法:在生成初始顺串时,可以使用替换选择算法。它利用一个内存中的最小堆,可以生成平均长度为2倍内存大小的顺串,从而减少初始顺串的数量,进而可能减少归并的趟数。
- 双缓冲或多缓冲:在进行输入输出操作时(读顺串、写顺串、写最终结果),可以使用双缓冲技术来重叠输入输出操作和CPU计算(排序或归并),提高效率。例如,当CPU处理一个缓冲区的数据时,另一个缓冲区可以同时进行磁盘读写。
- 输入输出性能:外部排序的性能瓶颈通常在磁盘输入输出。因此,应尽量使用顺序输入输出,并选择合适的块大小(缓冲区大小)来优化磁盘读写效率。
- 内存分配:10MB内存的划分需要仔细权衡。在归并阶段,更多的路数(K)通常更好,但这意味着每个输入缓冲区会变小。太小的缓冲区会导致频繁的输入输出。
总结来说,对于1亿数据和10MB内存的排序问题,标准答案是采用外部多路归并排序。通过先将数据分块在内存中排序生成初始顺串,然后再对这些顺串进行多路归并,最终得到完全排序的结果。核心在于有效地管理内存和磁盘输入输出。
拓展延申:
- 并行外部排序:如果有多核CPU和多个磁盘,可以进一步并行化外部排序的各个阶段,例如并行生成初始顺串,或者在归并阶段并行读取不同的顺串数据。
- 分布式排序:如果数据量更大,或者有分布式计算环境(例如 Hadoop MapReduce 或 Spark 这样的系统),则会采用分布式排序策略。其基本思想与外部排序类似,但会在多台机器上并行进行数据分片、局部排序(通常在映射阶段完成)和合并(通常在规约阶段完成)。
- 特定数据类型的优化:如果数据有特定分布或特性(例如,大量重复值),可以考虑一些针对性的优化,如在初始排序阶段进行计数排序或桶排序的变种,或者在归并时处理重复值。