我尝试使用辅助存储实现基于磁盘的合并排序。实现如下。
fd - 数据集的文件描述符将被排序
fd2 - 辅助存储的文件描述符
#define LENGTH 100
#define SEARCH_BEGIN 4
int merge_sort_d(int fd, int fd2, int s, int e) {
int i, m;
int l, r;
char lv[LENGTH], rv[LENGTH];
char buf[LENGTH];
if (s >= e) return 1;
m = (s + e) / 2;
merge_sort_d(fd, fd2, s, m);
merge_sort_d(fd, fd2, m+1, e);
l = s;
r = m+1;
memset(lv, 0, LENGTH);
memset(rv, 0, LENGTH);
lseek(fd2, 0, SEEK_SET);
while (l <= m && r <= e) {
lseek(fd, 1LL*SEARCH_BEGIN + 1LL*l*LENGTH, SEEK_SET);
read(fd, (void *)lv, LENGTH);
lseek(fd, 1LL*SEARCH_BEGIN + 1LL*r*LENGTH, SEEK_SET);
read(fd, (void *)rv, LENGTH);
if (strncmp(lv, rv, LENGTH) < 0) {
write(fd2, (void *)lv, LENGTH);
++l;
} else {
write(fd2, (void *)rv, LENGTH);
++r;
}
}
for (; l <= m; ++l) {
lseek(fd, 1LL*SEARCH_BEGIN + 1LL*l*LENGTH, SEEK_SET);
read(fd, (void *)lv, LENGTH);
write(fd2, (void *)lv, LENGTH);
}
for (; r <= e; ++r) {
lseek(fd, 1LL*SEARCH_BEGIN + 1LL*r*LENGTH, SEEK_SET);
read(fd, (void *)rv, LENGTH);
write(fd2, (void *)rv, LENGTH);
}
lseek(fd, 1LL*SEARCH_BEGIN + 1LL*s*LENGTH, SEEK_SET);
lseek(fd2, 0, SEEK_SET);
memset(buf, 0, LENGTH);
for (i=s; i<=e; ++i) {
read(fd2, (void *)buf, LENGTH);
write(fd, (void *)buf, LENGTH);
}
return 1;
}
在实现基于磁盘的合并排序后,我测试了一些小案例来检查它是否正确运行。它在小的情况下看起来足够快,但在运行它时使用超过20G的大型数据集(后来最终大小超过500G)。它需要2个小时,我很困惑它真的运行在O(nlogn)。当然,基于磁盘的算法和数据结构还存在一些额外的时间。
我很好奇它是否真的在O(nlogn)中运行。
答案 0 :(得分:0)
算法是O(N logN),但性能不仅仅是被排序的记录数。
不断寻求和文件访问非常慢。您应该在一个块中读取多个记录,因为读取16个记录(或200个)的时间与读取记录的时间差别不大。
在你的主要for循环中,当你已经拥有数据时,你正在读取数据。只有在新记录中读取(对应于l
或r
中的哪一个被更改)才会有很大帮助,尽管上面提到的多记录读数会好得多。
如果您使用大块(许多记录)而不是一次复制一个块,那么将数据从fd2
复制到fd
的最后一个循环将会快得多。这同样适用于中间的两个循环,其中您复制其中一个边的残余,并且r
循环是多余的,因为您在最后一个循环中立即复制相同的数据。
有关在磁盘上对大文件进行排序的所有详细信息,请参阅Knuth的计算机编程艺术的第5章(第3卷)。 (第二版第5.4节涉及外部排序。)
答案 1 :(得分:0)
标准的内存中合并排序会使log(n)传递数据,每次传递都会连续合并更大的列表。在第一次传递中,您合并包含每个项目的列表。接下来,它的列表包含两个项目,然后是四个,等等。使用此方法,您可以使log(n)遍历数据,并在每次传递期间查看n个项目。因此O(n log n)复杂度。
该方法对于内存中的排序非常有效,因为访问项目的成本不是很高。但是,对于基于磁盘的排序,它变得非常昂贵,因为访问每个项目的成本非常高。基本上,您正在读取和写入整个文件log(n)次。如果您有20 GB的100字节记录,那么您正在通过文件传递25次或更多次。因此,您的排序时间将由读取和写入文件25次所需的时间占主导地位。
外部种类是一种非常不同的动物。我们的想法是尽可能减少磁盘I / O.你分两次通过。在第一遍中,您尽可能多地将数据读入内存并使用Quicksort,合并排序或其他内存中排序算法对其进行排序,并将该块写入磁盘。然后,您将文件的下一个块读入内存,对其进行排序,然后写入磁盘。
完成第一次传递后,磁盘上会有一些已排序的块。如果您有一个20 GB的文件,并且您的计算机上有4 GB的可用内存,那么您将拥有五个块,每个块大小约为4 GB。 (请注意,您实际上可能有五个小于4 GB的块,而第六个非常小的块)。调用块数k
。
请注意,在第一次传递完成后,您已经读取并写入了一次每条记录。
在第二遍中,合并多个块。这是通过一堆k
项完成的。我们的想法是使用每个块中的第一个项初始化堆。您可以选择最小的k
项(位于堆顶部),并将其写入输出文件。然后,从包含刚刚删除的项的块中获取下一个项,并将其添加到堆中。重复此过程,直到清空所有块为止。
第一遍是O(n log n)。实际上,它是k *((n / k)log(n / k)),它可以达到n log n。第二遍是O(n log k)。
这里的重要部分是,在第二遍中,您再次阅读并写下每个项目一次。您已经将读取和写入每个项目log(n)次的磁盘I / O减少到读取和写入每个项目两次。这将比您编写的代码更快地运行很多。
同样重要的是要注意两种算法确实被认为是O(n log n)。 I / O常量是杀手。第二种算法实际上做了更多的计算,但它节省了大量的磁盘I / O时间,比理论上更快的算法更快。
维基百科上的External sorting文章给出了一个不错的解释,GeeksforGeeks article给出了一个C ++的工作示例。