问题描述:
输入:一个最多含有n个不重复的正整数(也就是说可能含有少于n个不重复正整数)的文件,其中每个数都小于等于n,且n=10^7。
输出:得到按从小到大升序排列的包含所有输入的整数的列表。
条件:最多有大约1MB的内存空间可用,但磁盘空间足够。且要求运行时间在5分钟以下,10秒为最佳结果。
分析:下面咱们来一步一步的解决这个问题,
1、归并排序。你可能会想到把磁盘文件进行归并排序,但题目要求你只有1MB的内存空间可用,所以,归并排序这个方法不行。
2、位图方案。熟悉位图的朋友可能会想到用位图来表示这个文件集合。例如正如编程珠玑一书上所述,用一个20位长的字符串来表示一个所有元素都小于20的简单的非负整数集合,边框用如下字符串来表示集合{1,2,3,5,8,13}:
0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
上述集合中各数对应的位置则置1,没有对应的数的位置则置0。
参考编程珠玑一书上的位图方案,针对我们的10^7个数据量的磁盘文件排序问题,我们可以这么考虑,由于每个7位十进制整数表示一个小于1000万的整 数。我们可以使用一个具有1000万个位的字符串来表示这个文件,其中,当且仅当整数i在文件中存在时,第i位为1。采取这个位图的方案是因为我们面对的 这个问题的特殊性:1、输入数据限制在相对较小的范围内,2、数据没有重复,3、其中的每条记录都是单一的整数,没有任何其它与之关联的数据。
所以,此问题用位图的方案分为以下三步进行解决:
- 第一步,将所有的位都置为0,从而将集合初始化为空。
- 第二步,通过读入文件中的每个整数来建立集合,将每个对应的位都置为1。
- 第三步,检验每一位,如果该位为1,就输出对应的整数。
经过以上三步后,产生有序的输出文件。令n为位图向量中的位数(本例中为1000 0000),程序可以用伪代码表示如下:
- //磁盘文件排序位图方案的伪代码
- //copyright@ Jon Bentley
- //July、updated,2011.05.29。
-
- //第一步,将所有的位都初始化为0
- for i ={0,....n}
- bit[i]=0;
- //第二步,通过读入文件中的每个整数来建立集合,将每个对应的位都置为1。
- for each i in the input file
- bit[i]=1;
-
- //第三步,检验每一位,如果该位为1,就输出对应的整数。
- for i={0...n}
- if bit[i]==1
- write i on the output file
//磁盘文件排序位图方案的伪代码 //copyright@ Jon Bentley //July、updated,2011.05.29。 //第一步,将所有的位都初始化为0 for i ={0,....n} bit[i]=0; //第二步,通过读入文件中的每个整数来建立集合,将每个对应的位都置为1。 for each i in the input file bit[i]=1; //第三步,检验每一位,如果该位为1,就输出对应的整数。 for i={0...n} if bit[i]==1 write i on the output file
上面只是为了简单介绍下位图算法的伪代码之抽象级描述。显然,咱们面对的问题,可不是这么简单。下面,我们试着针对这个要分两趟给磁盘文件排序的具体问题编写完整代码,如下。
- //copyright@ yansha
- //July、2010.05.30。
- //位图方案解决10^7个数据量的文件的排序问题
- //如果有重复的数据,那么只能显示其中一个 其他的将被忽略
- #include <iostream>
- #include <bitset>
- #include <assert.h>
- #include <time.h>
- using namespace std;
-
- const int max_each_scan = 5000000;
-
- int main()
- {
- clock_t begin = clock();
- bitset<max_each_scan> bit_map;
- bit_map.reset();
-
- // open the file with the unsorted data
- FILE *fp_unsort_file = fopen("data.txt", "r");
- assert(fp_unsort_file);
- int num;
-
- // the first time scan to sort the data between 0 - 4999999
- while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
- {
- if (num < max_each_scan)
- bit_map.set(num, 1);
- }
-
- FILE *fp_sort_file = fopen("sort.txt", "w");
- assert(fp_sort_file);
- int i;
-
- // write the sorted data into file
- for (i = 0; i < max_each_scan; i++)
- {
- if (bit_map[i] == 1)
- fprintf(fp_sort_file, "%d ", i);
- }
-
- // the second time scan to sort the data between 5000000 - 9999999
- int result = fseek(fp_unsort_file, 0, SEEK_SET);
- if (result)
- cout << "fseek failed!" << endl;
- else
- {
- bit_map.reset();
- while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
- {
- if (num >= max_each_scan && num < 10000000)
- {
- num -= max_each_scan;
- bit_map.set(num, 1);
- }
- }
- for (i = 0; i < max_each_scan; i++)
- {
- if (bit_map[i] == 1)
- fprintf(fp_sort_file, "%d ", i + max_each_scan);
- }
- }
-
- clock_t end = clock();
- cout<<"用位图的方法,耗时:"<<endl;
- cout << (end - begin) / CLK_TCK << "s" << endl;
- fclose(fp_sort_file);
- fclose(fp_unsort_file);
- return 0;
- }
而后测试了一下上述程序的运行时间,采取位图方案耗时14s,即14000ms:
本章中,生成大数据量(1000w)的程序如下,下文第二节的多路归并算法的c++实现和第三节的磁盘文件排序的编程实现中,生成的1000w数据量也是用本程序产生的,且本章内生成的1000w数据量的数据文件统一命名为“data.txt”。
- //purpose: 生成随机的不重复的测试数据
- //copyright@ 2011.04.19 yansha
- //1000w数据量,要保证生成不重复的数据量,一般的程序没有做到。
- //但,本程序做到了。
- //July、2010.05.30。
- #include <iostream>
- #include <time.h>
- #include <assert.h>
- using namespace std;
-
- const int size = 10000000;
- int num[size];
-
- int main()
- {
- int n;
- FILE *fp = fopen("data.txt", "w");
- assert(fp);
-
- for (n = 1; n <= size; n++)
- //之前此处写成了n=0;n<size。导致下面有一段小程序的测试数据出现了0,特此订正。
- num[n] = n;
- srand((unsigned)time(NULL));
- int i, j;
-
- for (n = 0; n < size; n++)
- {
- i = (rand() * RAND_MAX + rand()) % 10000000;
- j = (rand() * RAND_MAX + rand()) % 10000000;
- swap(num[i], num[j]);
- }
-
- for (n = 0; n < size; n++)
- fprintf(fp, "%d ", num[n]);
- fclose(fp);
- return 0;
- }
//purpose: 生成随机的不重复的测试数据 //copyright@ 2011.04.19 yansha //1000w数据量,要保证生成不重复的数据量,一般的程序没有做到。 //但,本程序做到了。 //July、2010.05.30。 #include <iostream> #include <time.h> #include <assert.h> using namespace std; const int size = 10000000; int num[size]; int main() { int n; FILE *fp = fopen("data.txt", "w"); assert(fp); for (n = 1; n <= size; n++) //之前此处写成了n=0;n<size。导致下面有一段小程序的测试数据出现了0,特此订正。 num[n] = n; srand((unsigned)time(NULL)); int i, j; for (n = 0; n < size; n++) { i = (rand() * RAND_MAX + rand()) % 10000000; j = (rand() * RAND_MAX + rand()) % 10000000; swap(num[i], num[j]); } for (n = 0; n < size; n++) fprintf(fp, "%d ", num[n]); fclose(fp); return 0; }
不过很快,我们就将意识到,用此位图方法,严格说来还是不太行,空间消耗10^7/8还是大于1M(1M=1024*1024空间,小于10^7/8)。
既然如果用位图方案的话,我们需要约1.25MB(若每条记录是8位的正整数的话,则10000000/(1024*1024*8) ~= 1.2M)的空间,而现在只有1MB的可用存储空间,那么究竟该作何处理呢?
updated && correct:
@yansha: 上述的位图方案,共需要扫描输入数据两次,具体执行步骤如下:
- 第一次,只处理1—4999999之间的数据,这些数都是小于5000000的,对这些数进行位图排序,只需要约5000000/8=625000Byte,也就是0.625M,排序后输出。
- 第二次,扫描输入文件时,只处理4999999-10000000的数据项,也只需要0.625M(可以使用第一次处理申请的内存)。
因此,总共也只需要0.625M
位图的的方法有必要强调一下,就是位图的适用范围为针对不重复的数据进行排序,若数据有重复,位图方案就不适用了。
3、多路归并。把 这个文件分为若干大小的几块,然后分别对每一块进行排序,最后完成整个过程的排序。k趟算法可以在kn的时间开销内和n/k的空间开销内完成对最多n个小 于n的无重复正整数的排序。比如可分为2块(k=2,1趟反正占用的内存只有1.25/2M),1~4999999,和5000000~9999999。 先遍历一趟,首先排序处理1~4999999之间的整数(用5000000/8=625000个字的存储空间来排序0~4999999之间的整数),然后 再第二趟,对5000001~1000000之间的整数进行排序处理。在稍后的第二节、第三节、第四节,我们将详细阐述并实现这种多路归并排序磁盘文件的 方案。
4、读者思考。经过上述思路3的方案之后,现在有两个局部有序的数组了,那么要得到一个完整的 排序的数组,接下来改怎么做呢?或者说,如果是K路归并,得到k个排序的子数组,把他们合并成一个完整的排序数组,如何优化?或者,我再问你一个问题,K 路归并用败者树 和 胜者树 效率有什么差别?这些问题,请读者思考。
第二节、多路归并算法的c++实现
本节咱们暂抛开咱们的问题,阐述下有关多路归并算法的c++实现问题。在稍后的第三节,咱们再来具体针对咱们的磁盘文件排序问题阐述与实现。
在了解多路归并算法之前,你还得了解归并排序的过程,因为下面的多路归并算法就是基于这个流程的。其实归并排序就是2路归并,而多路归并算法就是把2换成 了k,即多(k)路归并。下面,举个例子来说明下此归并排序算法,如下图所示,我们对数组8 3 2 6 7 1 5 4进行归并排序:
归并排序算法简要介绍:
一、思路描述:
设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
二路归并排序的过程是:
(1)把无序表中的每一个元素都看作是一个有序表,则有n个有序子表;
(2)把n个有序子表按相邻位置分成若干对(若n为奇数,则最后一个子表单独作为一组),每对中的两个子表进行归并,归并后子表数减少一半;
(3)反复进行这一过程,直到归并为一个有序表为止。
二路归并排序过程的核心操作是将一维数组中相邻的两个有序表归并为一个有序表。
二、分类:
归并排序可分为:多路归并排序、两路归并排序 。
若归并的有序表有两个,叫做二路归并。一般地,若归并的有序表有k个,则称为k路归并。二路归并最为简单和常用,既适用于内部排序,也适用于外部排序。本文着重讨论外部排序下的多(K)路归并算法。
三、算法分析:
1、稳定性:归并排序是一种稳定的排序。
2、存储结构要求:可用顺序存储结构。也易于在链表上实现。
3、时间复杂度: 对长度为n的文件,需进行lgn趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。。
4、空间复杂度:需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
注意:若用单链表做存储结构,很容易给出就地的归并排序。
总结:与快速排序相比,归并排序的最大特点是,它是一种稳定的排序方法。归并排序一般多用于外排序。但它在内排方面也占有重要地位,因为它是基于比较的时 间复杂度为O(N*Log(N))的排序算法中唯一稳定的排序,所以在需要稳定内排序时通常会选择归并排序。归并排序不要求对序列可以很快地进行随机访 问,所以在链表排序的实现中很受欢迎。
好的,介绍完了归并排序后,回到咱们的问题。由第一节,我们已经知道,当数据量大到不适合在内存中排序时,可以利用多路归并算法对磁盘文件进行排序。
本文出自:http://hi.baidu.com/duanpengtao8/blog/item/9c719218073d6f058618bf94.html