11如何给1千万个数据量的磁盘文件排序

问题描述:

输入:给定一个文件,里面含有多个不重复的正整数,其中每个数都小于等于n,并且正整数的总个数小于n,n=10^7。

输出:得到按从小到大升序排列的包含所有输入的整数的列表。

条件:最多有大约1MB的内存空间可用,但磁盘空间足够。且要求运行时间在5分钟以下,10秒为最佳结果。

 

一:位图法

    因为所有整数均不重复,所以,考虑位图法。位图法就是当且仅当整数i在文件中存在时,第i位置为1。

    因为n = 10000000,所以总共需要n/8个字节存储,也就是10000000/8 = 1250000个字节,大约1.25M,超过了内存限制。此时该如何处理呢?

 

    可以分批处理,具体执行步骤如下

    第一次,只处理0-4999999之间的数据,这些数都是小于5000000的,对这些数进行位图排序,只需要约5000000/8=625000字节,也就是0.625M,排序后输出。

    第二次,扫描输入文件时,只处理4999999-10000000的数据项,也只需要0.625M(可以使用第一次处理申请的内存)。

    因此,总共也只需要0.625M。位图的的方法有必要强调一下,就是位图的适用范围为针对不重复的数据进行排序,若数据有重复,位图方案就不适用了。

 

代码如下:

#define MAXNUM  10000000

void bit_set(unsigned char *bitset, int i)

{

         int  index = i/8;

         int  bit = i%8;

         unsigned  char bitmask = 0;

         bitmask = (unsigned char)1 << (7-bit);

         bitset[index] = bitset[index] | bitmask;

}

 

int isbitseted(unsigned char *bitset, int i)

{

         int  index = i/8;

         int  bit = i%8;

         unsigned  char bitmask = 0;

         bitmask= (unsigned char)1 << (7-bit);

         return (bitset[index] & bitmask) == bitmask;

}

 

int size = (MAXNUM/2)/8 + 1;

int i;

bitset = malloc(size);

memset(bitset, 0, size);

 

FILE *fp = fopen(DATAFILE, "r");

assert(fp);

FILE *res = fopen(RESFILE, "w");

assert(res);

 

while(fscanf(fp, "%d", &num) !=EOF)

{

         if(num <MAXNUM/2)

         {

                  bit_set(bitset,num);

         }

}

 

for(i = 0;i < MAXNUM/2; i++)

{

         if(isbitseted(bitset,i))

         {

                  fprintf(res,"%d ", i);

         }

}

 

fseek(fp, 0, SEEK_SET);

memset(bitset, 0, size);

while(fscanf(fp, "%d", &num) !=EOF)

{

         if(num >= MAXNUM/2 && num< MAXNUM)

         {

                  num = num-MAXNUM/2;

                  bit_set(bitset, num);

         }

}

 

for(i = 0; i < MAXNUM/2; i++)

{

         if(isbitseted(bitset,i))

         {

                  fprintf(res,"%d ", i+MAXNUM/2);

         }

}

       该算法在CPU为Pentium(R) Dual-Core E5500 2.80GHz上测试时间为6.359 sec

 

二:多路归并

    虽然位图法清晰明了,但是限制条件较多。比如整数不能重复,整数值不能太大,如果不满足这样的条件怎么办?

    分而治之,大而化小,也就是把整个大文件分为若干大小的几块,然后分别对每一块进行排序,最后完成整个过程的排序。这就是多路归并算法。

 

    假设文件中整数个数为N(N是亿级的),整数之间用空格分开。首先分多次从该文件中读取M个整数,每次将M个整数在内存中使用快速排序之后存入临时文件,然后使用多路归并将各个临时文件中的数据再次整体排好序后存入输出文件。显然,该排序算法需要对每个整数做2次磁盘读和2次磁盘写。

 

    针对本题目,本程序由两部分构成:

    1:内存排序:

    由于要求的可用内存为1MB,那么每次可以在内存中对250K个数据进行排序,然后将有序的数写入硬盘。那么10M个数据需要循环40次,最终产生40个有序的文件。

    2:归并排序:

    将每个文件最开始的数读入(由于有序,所以为该文件最小数),存放在一个大小为40的first_data数组中;

    选择first_data数组中最小的数min_data,及其对应的文件索引index,将first_data数组中最小的数写入文件result,然后更新数组first_data(根据index读取该文件下一个数代替min_data)。直到所有40个文件读取完毕。

 

代码如下:

#define FILENUM 40

#define MAXNUM  10000000

 

void memorysort()

{

     int i, j, k;

     int *buf = NULL;

 

     int size;    //size表示文件内需要存放的整数个数,也就是内存数组的大小

     if(MAXNUM % FILENUM)

     {

              size = sizeof(int) * (MAXNUM/FILENUM + 1);

     }

     else

     {

              size= sizeof(int) * MAXNUM/FILENUM;

     }

     buf = malloc(size);

     assert(buf);

 

     memset(buf, 0, size);

     FILE *fp = fopen("data.txt","r");

     assert(fp);

 

     char filename[10] = {};

     FILE *fileset[FILENUM+1] = {NULL};

 

     for(i = 1; i <= FILENUM; i++)  //创建打开40个临时文件,文件名为:s1,s2,...,s40

     {

              sprintf(filename,"s%d", i);

              fileset[i]= fopen(filename, "w");

              assert(fileset[i]);

     }

 

     i = 0;

     j = 1;

     k = 0;

     while(fscanf(fp, "%d", &buf[i])!= EOF)

     {

         i++;

         if(i>= (size/sizeof(int)))  //读取完一个文件所需整数数。

         {

                  quicksort(buf,0, i-1);

                  for(k= 0; k < i; k++)

                 {

                          fprintf(fileset[j],"%d ", buf[k]);

                  }

                  i = 0;

                  fclose(fileset[j++]);

                  memset(buf,0, size);

         }

     }

     if(i < (size/sizeof(int)) && i> 0)       //这是针对MAXNUM/FILENUM不能整除的情况。

     {

         quicksort(buf,0, i-1);

         for(k= 0; k < i; k++)

         {

                  fprintf(fileset[j],"%d ", buf[k]);

         }

     }

}

 

void  externsort()

{

     FILE *fout = NULL;

     fout = fopen("res", "w");

     assert(fout);

 

     int *buf = NULL;

     buf = malloc((FILENUM+1) * sizeof(int));

     assert(buf);

 

     int i;

     char filename[10] = {};

     int min, index;

     int num = 0;

        

     FILE *fileset[FILENUM+1] = {NULL};

     for(i = 1; i <= FILENUM; i++)

     {

         sprintf(filename,"s%d", i);

         fileset[i]= fopen(filename, "r");

         assert(fileset[i]);

     }

 

     for(i = 1; i <= FILENUM; i++)  //首先读取每个文件的第一个数,放入数组中。

     {

         //若文件读取完毕,则置响应的数组元素为-1

         if(fscanf(fileset[i],"%d", &buf[i]) == EOF)    

         {

                  buf[i]= -1;

         }

     }

 

     for(;;)

     {      

         min= MAXNUM;

         index= -1;

         for(i= 1; i <= FILENUM; i++)  //min记录数组中的最小值,index记录最小值的索引

         {

                  if(buf[i] != -1 && min > buf[i])

                  {

                          min= buf[i];

                          index= i;

                  }

         }

         fprintf(fout,"%d ", min);

         //继续读取响应的文件,更新buf数组

 

         if(fscanf(fileset[index],"%d", &buf[index]) == EOF)             

        {

                  buf[index]= -1;

                  num++;

                  if(num== FILENUM)       //所有文件读取完毕,退出循环

                  {

                          break;

                  }

         }

    }

}

         该算法在CPU为Pentium(R) Dual-Core E5500 2.80GHz上测试时间为11.101 sec,频繁的读写文件造成了速度要比位图法慢很多。

       注意:上述程序在外部归并排序的过程中,每次选择最小值的时候总是将所有数组元素进行比较,是一种最简单的做法,其实可以建立败者树,使得总比较次数达到n*logk。

 

(http://blog.csdn.net/v_JULY_v/article/details/6451990)

原文地址:https://www.cnblogs.com/gqtcgq/p/7247176.html