问题描述:
输入:一个最多含有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),程序可以用伪代码表示如下:
//磁盘文件排序位图方案的伪代码
//第一步,将所有的位都初始化为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
上面只是为了简单介绍下位图算法的伪代码之抽象级描述。显然,咱们面对的问题,可不是这么简单。下面,我们试着针对这个要分两趟给磁盘文件排序的具体问题编写完整代码,如下。
#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();
FILE *fp_unsort_file=fopen("data.txt","r");
assert(fp_unsort_file);
int num;
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;
for(i=0;i<max_each_scan;i++)
{
if(bit_map[i]==1)
fprintf(fp_sort_file,"%d ",i);
}
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;
}
接下来,我们来利用归并排序编程实现上述磁盘文件排序的问题,本程序由两部分构成:
1、内存排序
由于要求的可用内存为1MB,那么每次可以在内存中对250K的数据进行排序,然后将有序的数写入硬盘。
那么10M的数据需要循环40次,最终产生40个有序的文件。
2、归并排序
- 将每个文件最开始的数读入(由于有序,所以为该文件最小数),存放在一个大小为40的first_data数组中;
- 选择first_data数组中最小的数min_data,及其对应的文件索引index;
- 将first_data数组中最小的数写入文件result,然后更新数组first_data(根据index读取该文件下一个数代替min_data);
- 判断是否所有数据都读取完毕,否则返回2。
所以,本程序按顺序分两步,第一步、Memory Sort,第二步、Merge Sort。程序的流程图,如下图所示(感谢F的绘制)。
完整的程序如下:
#include<iostream>
#include<string>
#include<algorithm>
#include<time.h>
using namespace std;
int sort_num=10000000;
int memory_size=250000;
int read_data(FILE *fp,int *space)
{
int index=0;
while(index<memory_size && fscanf(fp,"%d ",&space[index])!=EOF)
index++;
return index;
}
void write_data(FILE *fp,int *space,int num)
{
int index=0;
while(index<num)
{
fprintf(fp,"%d ",space[index]);
index++;
}
}
void check_fp(FILE *fp)
{
if(fp==NULL)
{
cout<<"The file pointer is invalid!"<<endl;
exit(1);
}
}
int compare(const void *first_num,const void *second_num)
{
return *(int *)first_num-*(int *)second_num;
}
string new_file_name(int n)
{
char file_name[20];
sprintf(file_name,"data%d.txt",n);
return file_name;
}
int memory_sort()
{
FILE *fp_in_file=fopen("data.txt","r");
check_fp(fp_in_file);
int counter=0;
while(true)
{
int *space=new int[memory_size];
int num=read_data(fp_in_file,space);
if(num==0)
break;
qsort(space,num,sizeof(int),compare);
string file_name=new_file_name(++counter);
FILE *fp_aux_file=fopen(file_name.c_str(),"w");
check_fp(fp_aux_file);
write_data(fp_aux_file,space,num);
fclose(fp_aux_file);
delete[] space;
}
fclose(fp_in_file);
return counter;
}
void merge_sort(int file_num)
{
if(file_num<=0)
return;
FILE *fp_out_file=fopen("result.txt","w");
check_fp(fp_out_file);
FILE **fp_array=new FILE *[file_num];
int i;
for(i=0;i<file_num;i++)
{
string file_name=new_file_name(i+1);
fp_array[i]=fopen(file_name.c_str(),"r");
check_fp(fp_array[i]);
}
int *first_data=new int[file_num];
bool *finish=new bool[file_num];
memset(finish,false,sizeof(bool)*file_num);
for(i=0;i<file_num;i++)
fscanf(fp_array[i],"%d ",&first_data[i]);
while(true)
{
int index=0;
while(index<file_num&&finish[index])
index++;
if(index>=file_num)
break;
int min_data=first_data[index];
for(i=index+1;i<file_num;i++)
{
if(min_data>first_data[i]&&!finish[i])
{
min_data=first_data[i];
index=i;
}
}
fprintf(fp_out_file,"%d ",min_data);
if(fscanf(fp_array[index],"%d ",&first_data[index]))
finish[index]=true;
}
fclose(fp_out_file);
delete []finish;
delete []first_data;
for (i = 0; i < file_num; i++)
fclose(fp_array[i]);
delete [] fp_array;
}
int main()
{
clock_t start_memory_sort=clock();
int aux_file_num=memory_sort();
clock_t end_memory_sort=clock();
cout<<"The time needs in memeory sort: "<<end_memory_sort-start_memory_sort<<endl;
clock_t start_merge_sort = clock();
merge_sort(aux_file_num);
clock_t end_merge_sort = clock();
cout << "The time needs in merge sort: " << end_merge_sort - start_merge_sort << endl;
system("pause");
return 0;
}
参考来源:
1. 《编程珠玑》
2. http://blog.csdn.net/v_july_v/article/details/6451990