下面的问题描述及相关文字参考于CSDN中JULY的博客,在此对JULY表示感谢。JULY的博客地址如下:
http://blog.csdn.net/v_JULY_v/article/details/6451990
1. 问题描述
输入:一个最多最多含有n个正整数的大文件,并且不含重复元素,每个数都小于等于n,且n=10^7
输出:得到按从小到大升序排列的输出序列
条件:最多含有约1MB的内存空间,但磁盘空间足够
2. 算法描述
由于本题的特殊性,不含重复的正整数,因此可以考虑位图排序
基本思路是:用一个bit的0/1来标志一个数据, 如果数据存在就置为1,最后遍历所有位,检查是否存在该数据,如果存在直接输出,输出的序列已经有序,这里需要标志10^7个数据,需要内存10^7/8=1.25MB,但是题目要求最多1MB内存空间。这里稍作变通,分两次读入数据,第一次遍历读入大小在0-49999990的数据,遍历位图输出,第二次读入大小在5000000-10000000的数据,遍历位图输出,每次使用内粗0.625MB,满足题意。
这里使用STL的bitset会更简单
#include <stdio.h> #include <stdlib.h> #define width 5000000 int bit[(width>>5)+1]; /* 注意:移位操作符优先级比算术操作符低 */ void clear(int i) { bit[i>>5] &= ~(1<<(i%32)); } void set(int i) { bit[i>>5] |= 1 <<(i & 31); } int test(int i) { return bit[i>>5]&(1<<(i & 31)); } int main() { FILE * fpin, *fpout; int d; fpin=fopen("in","r"); if(fpin==NULL) exit(1); for(int i=0;i<width;i++) clear(i); /* 由于内存容量限制,将磁盘文件分两批排序,第一匹先排序前半部分 */ while(fscanf(fpin,"%d", &d)!=EOF) if(d < width) set(d); fpout=fopen("out","w"); if(fpout==NULL) exit(1); for(int i=0;i<width;i++) if(test(i)) fprintf(fpout,"%d\n",i); int length=fseek(fpin,0,SEEK_SET); if(length) /* 定位正确返回0 */ exit(1); for(int i=0;i<width;i++) clear(i); while(fscanf(fpin,"%d", &d)!=EOF) if(d >= width && d < 10000000) set(d-width); for(int i=0;i<width;i++) if(test(i)) fprintf(fpout,"%d\n",i+width); return 0; }
位图排序速度很快,需要kn次遍历和n/k的内存空间,但是要求没有重复元素,当数据中有重复元素的时候位图排序就不好用了, 下面介绍借助败者树实现的外部多路归并排序。
假设文件中整数个数为N(N是亿级的),整数之间用空格分开。首先分多次从该文件中读取M(十万级)个整数,每次将M个整数在内存中使用内部排序之后存入临时文件,这样就得到多个外部文件,对应于多个外部文件,我们可以利用多路归并将各个临时文件中的数据一边读入内存,一边进行归并输出到输出文件。显然,该排序算法需要对每个整数做2次磁盘读和2次磁盘写
归并多个临时文件的过程中,从每个临时文件中读入一个值,它们是对应临时文件中的最小值,可以采用遍历的方法选出这些数据中的最小值,使用时间为kn,这里可以采用败者树的方法,这样每次选出最小值的时候不用和所有数值进行比较,使用时间为klogn
整体算法流程如下
/** * @file code.c * @author <kong@KONG-PC> * @date Sun Nov 25 21:25:36 2012 * * @brief 多路归并排序,这里是10路归并排序 * * */ #include <stdio.h> #include <assert.h> #include <time.h> #include <stdlib.h> int a[1000000]; /* 用来产生随机数 */ int b[100000]; /* 用来暂存数据 */ char file_name[100][20]; /* 临时存储的文件名 */ FILE * file_fp[100]; /* 指向临时存储文件的指针 */ #define K 10 /* K路归并排序 */ #define MIN -1 /* 初始化败者树的时候做特殊值 */ #define MAX 100000000 /* 当某个临时文件读完时,叶子节点赋值 * 为MAX,在程序中用来判断数据是否全部 * 读取完毕 */ int loserTree[K]; /* 存储败者树中间节点值,下标0处存储冠军节点 */ int leaves[K+1]; /* 败者树的叶子节点从下标1开始存储叶子节点值,下标0处 * 存储一个最小值节点 */ /** * 调整败者树 * * @param i 需要调整的叶子节点的下标 */ void adjust(int i) { int parent=(i+K-1)/2; /* 找到父节点 */ while(parent>0) { if(leaves[i]>leaves[loserTree[parent]]) /* 和父节点比较,找到 * 新的优胜者 */ { int temp=loserTree[parent]; loserTree[parent]=i; /* i指向的是优胜者 */ i= temp; } parent = parent / 2; } loserTree[0]=i; } void initLoserTree() { leaves[0]=MIN; for(int i=0;i<K;i++) loserTree[i]=0; for(int i=K;i>0;i--) adjust(i); } void swap(int * a, int * b) { int t= *a; *a=*b; *b=t; } int Rand(int begin, int end) { int t = rand() % (end - begin) + begin; return t; } /** * 生成1000000个不重复的随机数 * */ void rand_int() { FILE * fp; fp = fopen("in", "w"); assert(fp); srand(time(NULL)); for(int i=0;i<1000000;i++) a[i]=i; for(int i=0;i<1000000;i++) { int t = Rand(i, 1000000); swap(&a[i],&a[t]); } for(int i=0;i<1000000;i++) fprintf(fp, "%d ", a[i]); fclose(fp); } int cmp(const void * m, const void * n) { return *(int *)m - *(int *)n; } /** * 分K次读入文件,每次读入一部分数据,在内存中排完序之后,存入临时文件 * */ void split_data() { FILE * fp = fopen("in","r"); assert(fp); for(int i=0;i<10;i++) { sprintf(file_name[i], "%s%d%s","data",i,".dat"); file_fp[i] = fopen(file_name[i],"w"); for(int j=0;j<100000;j++) if(fscanf(fp,"%d",&b[j]) == EOF) exit(1); /* 采用快速排序的方法 */ qsort(b, 100000, sizeof(int), cmp); for(int j=0;j<100000;j++) if(fprintf(file_fp[i], "%d ", b[j]) == EOF) exit(1); fclose(file_fp[i]); } fclose(fp); } /** * 将临时文件归并,结合败者树,每次从一列数中找出最小值的时候只需要 * logn次比较,在时间要求不太明显的时候也可以遍历数组找出最值,比较次数 * 为n * */ void merge_data() { FILE * fp = fopen("out","w"); int i; for(i=0;i<10;i++) { file_fp[i]=fopen(file_name[i],"r"); assert(file_fp[i]); } /* 先从每个临时文件中读入一个数值,初始化叶子节点 */ for(i=0;i<10;i++) if(fscanf(file_fp[i],"%d", &leaves[i+1])==EOF) leaves[i+1]=MAX; /* 构造败者树 */ initLoserTree(); while(1) { int flag = loserTree[0]; int temp = leaves[flag]; /* 判断败者树的最小值,用来确定是否已 * 经完全读取完毕 */ if(temp==MAX) break; fprintf(fp, "%d ", temp); /* 输出最小值以后,再从响应的临时文件中读入一个数据,同时调整 * 败者树 */ if(fscanf(file_fp[flag-1], "%d", &leaves[flag]) == EOF) leaves[flag]=MAX; adjust(flag); } for(int i=0;i<10;i++) fclose(file_fp[i]); fclose(fp); } int main() { rand_int(); /* 产生随机数 */ split_data(); /* 将大文件分成多个分别有序的临时文件 */ merge_data(); /* 借助败者树将有序的临时文件归并在一起 */ return 0; }