外排序-处理极大量数据的排序算法--5 亿整数的大文件排序的思路

外排序

**外排序(External sorting)**是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。而后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。

外归并排序

外排序的一个例子是外归并排序(External merge sort),它读入一些能放在内存内的数据量,在内存中排序后输出为一个顺串(即是内部数据有序的临时文件),处理完所有的数据后再进行归并。比如,要对900 MB的数据进行排序,但机器上只有100 MB的可用内存时,外归并排序按如下方法操作:

  1. 读入100 MB的数据至内存中,用某种常规方式(如快速排序、堆排序、归并排序等方法)在内存中完成排序。
  2. 将排序完成的数据写入磁盘。
  3. 重复步骤1和2直到所有的数据都存入了不同的100 MB的块(临时文件)中。在这个例子中,有900 MB数据,单个临时文件大小为100 MB,所以会产生9个临时文件。
  4. 读入每个临时文件(顺串)的前10 MB( = 100 MB / (9块 + 1))的数据放入内存中的输入缓冲区,最后的10 MB作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得更好的效果。)
  5. 执行九路归并算法(见后面的多路排序),将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写出至目标文件,清空缓冲区。一旦9个输入缓冲区中的一个变空,就从这个缓冲区关联的文件,读入下一个10M数据,除非这个文件已读完。这是“外归并排序”能在主存外完成排序的关键步骤 – 因为“归并算法”(merge algorithm)对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。

为了增加每一个有序的临时文件的长度,可以采用置换选择排序(Replacement selection sorting)(可以可以先看下一项目录)。它可以产生大于内存大小的顺串。具体方法是在内存中使用一个最小堆进行排序,设该最小堆的大小为M(对标前面的100M内存)。算法描述如下:

  1. 初始时将输入文件读入内存,建立最小堆。
  2. 将堆顶元素输出至输出缓冲区。然后读入下一个记录:
    1. 若该元素的关键码值不小于刚输出的关键码值,将其作为堆顶元素并调整堆,使之满足堆的性质;
    2. 否则将新元素放入堆底位置,将堆的大小减1。(这里wiki说的我不会实现,我觉得这里应该是插入到另一个最小堆用于生成下一轮顺串)
  3. 重复第2步,直至堆大小变为0。
  4. 此时一个顺串已经产生。将堆中的所有元素建堆(这里我的想法就是想2.2中讲的直接用另一个最小堆),开始生成下一个顺串。

此方法能生成平均长度为2M(对标等效于前面200M分文件)的顺串,可以进一步减少访问外部存储器的次数(原本访问两次存储器,现在平均只要访问一次了),节约时间,提高算法效率。


注:个人理解可能有偏差,可能有错误,希望大佬们不吝赐教,感谢


置换选择排序

我以前没有看过这种算法的详细描述,而是根据我从阅读这套讲义中学到的东西来进行分析的。

根据我的理解,选择排序和替换选择排序之间的主要区别在于,选择排序旨在对存储在主内存中的完整序列进行排序,而替换选择排序则用于将太大而无法放入主内存的未排序序列转换为内存。可存储在外部存储器中的一系列“序列”排序序列。然后可以将这些外部链合并在一起以形成整体排序序列。尽管它们的名称和算法操作的一两个关键步骤相似,但它们的设计目的是解决根本不同的问题。

选择排序
在线上有很多关于选择排序的好教程,因此我不会花太多时间讨论它。直观上,该算法的工作方式如下:

找到最小的元素并将其交换到数组的位置0。
找到第二个最小的元素,并将其交换到数组的位置1。
找到第三小的元素并将其交换到数组的位置2

找到第n个最小的元素,并将其交换到数组的位置n-1。
这假设数组可以完全保存在内存中,如果是这种情况,该算法将以Θ(n 2)时间运行。这不是非常快,对于大型数据集则不建议使用。

替换选择排序
该算法在1965年由Donald Knuth进行了描述,因此它的设计目的是在与我们目前所用的计算环境完全不同的计算环境中工作。计算机的内存很少(通常是一些固定数量的寄存器),但是可以访问大型外部驱动器。通常会构建一些算法,将一些值加载到寄存器中,在其中进行处理,然后将其直接刷新回外部存储。(有趣的是,这与处理器当前的工作方式类似,除了主存储器而不是外部存储器外)。

假设我们在内存中有足够的空间来容纳两个数组:第一个Values大小为n的数组可以容纳一堆值,第二个Active大小为n的数组可以容纳布尔值。考虑到我们只有足够的内存空间来容纳Activeand Values数组,以及一些额外的存储空间变量,我们将尝试采用大量未排序值的输入流,并尽力对它进行排序。

该算法背后的思想如下。首先,n将包含未排序序列的外部源的值直接加载到Values数组中。然后,将所有Active值设置为true。例如,如果n = 4,我们可能具有以下设置:

Values: 4 1 0 3
Active: Yes Yes Yes Yes
替换选择排序算法的工作原理是重复查找Values数组中的最小值并将其写出到输出流中。在这种情况下,我们首先查找0值并将其写入流。这给

Values: 4 1 3
Active: Yes Yes Yes Yes

Output: 0
现在,我们在Values数组中有一个空白点,因此我们可以从外部源中提取另一个值。假设我们得到2。在这种情况下,我们有以下设置:

Values: 4 1 2 3
Active: Yes Yes Yes Yes

Output: 0
注意,由于2> 0且0是此处的最小元素,因此可以保证在将0写入输出时,2不应早于它。那很好。因此,我们继续算法的下一步,并再次在此处找到最小的元素。那是1,所以我们将其发送到输出设备:

Values: 4 2 3
Active: Yes Yes Yes Yes

Output: 0 1
现在,从外部源中读取另一个值:

Values: 4 -1 2 3
Active: Yes Yes Yes Yes

Output: 0 1
现在我们有麻烦了。这个新值(-1)小于1,这意味着如果我们确实希望此值按排序顺序进入输出,则它应该位于1之前。但是,我们没有足够的内存来重新读取输出设备并进行修复。相反,我们将执行以下操作。现在,让我们将-1保留在内存中。我们将尽最大努力对其余元素进行排序,但是当我们这样做时,我们将进行第二次迭代以生成排序后的序列,并将-1放入该序列中。换句话说,我们将产生两个排序序列,而不是产生一个排序序列。

为了在内存中表明我们尚不希望写出-1,我们将标记-1的插槽标记为非活动状态。如图所示:

Values: 4 -1 2 3
Active: Yes NO Yes Yes

Output: 0 1
从现在开始,我们将假装-1不存在。

让我们继续前进。现在,我们在内存中找到仍处于活动状态的最小值(2),并将其写到设备中:

Values: 4 -1 3
Active: Yes NO Yes Yes

Output: 0 1 2
现在,我们从输入设备中提取下一个值。假设它是7:

Values: 4 -1 7 3
Active: Yes NO Yes Yes

Output: 0 1 2
由于7> 2,它在输出中的2之后,所以我们什么也不做。

在下一次迭代中,我们找到最低的有效值(3)并将其写出:

Values: 4 -1 7
Active: Yes NO Yes Yes

Output: 0 1 2 3
我们从输入设备中提取下一个值。假设它也是 3。在这种情况下,我们知道3是最小的值,因此我们可以直接将其写入输出流,因为3在这里是所有值中的最小的,因此我们可以保存一个迭代:

Values: 4 -1 7
Active: Yes NO Yes Yes

Output: 0 1 2 3 3
现在,我们从输入设备中提取下一个值。假设它是2。在这种情况下,和以前一样,我们知道2应该排在3之前。就像前面的-1一样,这意味着我们现在需要将2保留在内存中;我们稍后再写出来。现在,我们的设置如下所示:

Values: 4 -1 7 2
Active: Yes NO Yes NO

Output: 0 1 2 3 3
现在,我们找到最小的有效值(4)并将其写入输出设备:

Values: -1 7 2
Active: Yes NO Yes NO

Output: 0 1 2 3 3 4
假设我们现在读入1作为下一个输入。因此Values,我们将其放入,但将其标记为无效:

Values: 1 -1 7 2
Active: NO NO Yes NO

Output: 0 1 2 3 3 4
只有一个活动值,即7,因此我们将其写出:

Values: 1 -1 2
Active: NO NO Yes NO

Output: 0 1 2 3 3 4 7
假设我们现在读取一个5。在这种情况下,和以前一样,我们将其存储但将插槽标记为非活动:

Values: 1 -1 5 2
Active: NO NO NO NO

Output: 0 1 2 3 3 4 7
请注意,所有值现在都处于非活动状态。这意味着我们已经从内存中清除了所有可以进入当前输出运行的值。现在,我们需要去写出稍后持有的所有值。为此,我们将所有值标记为活动值,然后像以前一样重复:

Values: 1 -1 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7
-1是最小值,因此我们将其输出:

Values: 1 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1
假设我们读取的是3. -1 < 3,因此我们将其加载到Values数组中。

Values: 1 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1
1是此处的最小值,因此我们将其删除:

Values: 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1
假设我们现在没有输入值。我们将此插槽标记为已完成:

Values: — 3 5 2
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1
接下来是2:

Values: — 3 5 —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2
然后3:

Values: — — 5 —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2 3
最后,5:

Values: — — — —
Active: Yes Yes Yes Yes

Output: 0 1 2 3 3 4 7 -1 1 2 3 5
我们完成了!请注意,结果序列未排序,但比以前好很多。现在,它由按排序顺序的两个链组成。将它们合并在一起(以与我们为mergesort进行合并相同的方式)将对结果数组进行排序。该算法可能会产生更多的链,但是由于我们的样本输入很小,因此只有两个。

那这有多快?好吧,循环的每次迭代最多进行n次比较(在内存中),一次读取和一次写入。因此,如果流中总共有N个值,则该算法执行O(nN)个比较和O(N)个存储操作。如果内存操作很昂贵,那还算不错,尽管最后需要第二遍才能将所有内容合并在一起。

在伪代码中,算法如下所示:

Make Values an array of n elements.
Make Active an array of n booleans, all initially true.

Read n values from memory into Values.
Until no values are left to process:
    Find the smallest value that is still active.
    Write it to the output device.
    Read from the input device into the slot where the old element was.
    If it was smaller than the old element, mark the old slot inactive.
    If all slots are inactive, mark them all active.

如果现在有任何理由对此算法进行编码,我会感到震惊。几十年前,当内存真的很小的时候,这是有道理的。如今,有更好的外部排序算法可用(前面讲的第一种方法),并且几乎可以肯定它们的性能要优于该算法。(中文wiki说置换选择排序方法减少了落磁盘,更快,这个人说第一种外归并排序更快,但是我还是觉得减少落磁盘的可能置换选择排序方法更快一点)

多路排序

定义

k条路

暴力法

每次选取一个min都从k个有序串中取出最前面的值来进行 k-1次min(a,b)

最佳归并树+败者树

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

可以轻度参考

参考链接

https://en.wikipedia.org/wiki/External_sorting
https://zh.wikipedia.org/wiki/%E5%A4%96%E6%8E%92%E5%BA%8F
https://stackoverflow.com/questions/16326689/replacement-selection-sort-v-selection-sort

发布了41 篇原创文章 · 获赞 5 · 访问量 1万+
展开阅读全文

如何将多个文件(每个文件大于1G)字符串进行行为单位排序,并且排序时内存小于50M.

03-29

现有N个文件(N>5): • 每个文件包含了多行的字符串 • 每个文件大小大于1G • 文件内字符串随机排列 要求实现:一个外部排序算法,以行为单位排序,满足以下需求: 需求 • 用C/C++/Java/C#实现 • 提供编译文件,如: o GNU Makefile o Visual Studio 工程文件 o Eclipse工程文件 o MAVEN文件等 • 编译文件需要生成两个可执行文件,且满足下文的接口需求: o 测试文件生成程序: filesort_testgen o 排序程序: filesort • 排序程序使用内存不能大于50M • 提供README文件,内容至少包括如何编译工程 filesort_testgen命令行接口 filesort_testgen FILE_COUNT LINES_PER_FILE PREFIX 其中: • filesort_testgen为可执行文件名 • FILE_COUNT为需要生成的待排序文件的个数 • LINES_PER_FILE:每个待排序文件的行数 • PREFIX为生成文件的前缀 • 执行结果 其中${PREFIX}表示PREFIX参数的值 o 生成: ${PREFIX}1 ${PREFIX}2 ${PREFIX}3, 例子: > filesort_testgen 5 1000000 unsorted Generated unsorted files: unsorted1 unsorted2 unsorted3 unsorted4 unsorted5 结果会生出 unsorted1 unsorted2 unsorted3 unsorted4 unsorted5 filesort命令行接口 filesort INPUT_FILE_1 INPUT_FILE_2 ... OUTPUT_FILE 其中: • INPUT_FILE_1表示输入文件的文件名(或路径) • OUTPUT_FILE表示输出文件的文件名 • 执行结果: o 输出排序完的文件 o 输出使用了多少内存 > filesort unsorted1 unsorted2 unsorted3 unsorted4 unsorted5 sorted Generate sorted file: sorted Used memory: 4553333 B (4.34 M) 结果会生出排序后的文件sorted,并输出内存使用情况 问答

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术工厂 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览