zoukankan      html  css  js  c++  java
  • 海量数据多路归并排序的c++实现(归并时利用了败者树)

    海量数据多路归并排序的c++实现(归并时利用了败者树) - harryshayne - 博客园

        海量数据多路归并排序的c++实现(归并时利用了败者树)

        问题:如何给10^7个数据量的磁盘文件排序(《编程珠玑》第一章)

        下面的问题描述及相关文字都参考于CSDN中JULY的博客,在此对JULY表示感谢。JULY的博客地址如下:

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

        1、问题描述:
        输入:一个最多含有n个不重复的正整数(也就是说可能含有少于n个不重复正整数)的文件,其中每个数都小于等于n,且n=10^7。
        输出:得到按从小到大升序排列的包含所有输入的整数的列表。
        条件:最多有大约1MB的内存空间可用,但磁盘空间足够。且要求运行时间在5分钟以下,10秒为最佳结果。

        2、算法描述:

               在编程珠玑中,描述了三种解决方法,分别是外排多路归并法、多通道排序法和位图排序法,在待排序文件中不含重复数的情况下,位图排序法是最高效的,但在更一般的情况下,外排多路归并法具有通用性,因此,本文描述这种外排法。

               假设文件中整数个数为N(N是亿级的),整数之间用空格分开。首先分多次从该文件中读取M(十万级)个整数,每次将M个整数在内存中使用内部排序之后存入临时文件,这样就得到多个外部文件,对应于多个外部文件,我们可以利用多路归并将各个临时文件中的数据一边读入内存,一边进行归并输出到输出文件。显然,该排序算法需要对每个整数做2次磁盘读和2次磁盘写。(如果根据初始外部文件的个数设置归并的路数,则会对每个整数做多次读/写,具体次数可参考严蔚敏书籍)

        本算法的流程图如下:

        3、算法实现:

        下面是算法的具体实现,这是针对JULY算法的修改,因为在JULY的算法中,并没有利用败者树来进行归并排序,在每次选择多路文件在数组中的元素的最小值的时候,只是简单地遍历数组来进行选择,没有利用败者树来进行选择,因此在效率上会有一定的差别(对磁盘的访问效率是一样的,不同的是内部选择的时候)。

        
        View Code

         下面再献上JULY算法的原版(有些我修改的注释部分没删,目的是为了看代码的时候可以比较一下),读者可以对照关键部分的区别:
        View Code

        1 //copyright@ 纯净的天空 && yansha
        2 //5、July,updated,2010.05.28。
        3 #include <iostream>
        4 #include <ctime>
        5 #include <fstream>
        6 //#include "ExternSort.h"
        7 using namespace std;
        8
        9 //使用多路归并进行外排序的类
        10 //ExternSort.h
        11
        12 /*
        13 * 大数据量的排序
        14 * 多路归并排序
        15 * 以千万级整数从小到大排序为例
        16 * 一个比较简单的例子,没有建立内存缓冲区
        17 */
        18
        19 #ifndef EXTERN_SORT_H
        20 #define EXTERN_SORT_H
        21
        22 #include <cassert>
        23 //#define MIN -1//这里开始的时候出现了一个BUG,如果定义的MIN大于等于待排序的数,则会是算法出现错误
        24 //#define MAX 10000000//最大值,附加在归并文件结尾
        25 //typedef int* LoserTree;
        26 //typedef int* External;
        27
        28 class ExternSort
        29 {
        30 public:
        31 void sort()
        32 {
        33 time_t start = time(NULL);
        34
        35 //将文件内容分块在内存中排序,并分别写入临时文件
        36 int file_count = memory_sort(); //
        37
        38 //归并临时文件内容到输出文件
        39 merge_sort(file_count);
        40 //ls=new int[k];
        41 //b=new int[k+1];
        42 //K_Merge();
        43 //delete []ls;
        44 //delete []b;
        45
        46 time_t end = time(NULL);
        47 printf("total time:%f ", (end - start) * 1000.0/ CLOCKS_PER_SEC);
        48 }
        49
        50 //input_file:输入文件名
        51 //out_file:输出文件名
        52 //count: 每次在内存中排序的整数个数
        53 ExternSort(const char *input_file, const char * out_file, int count)
        54 {
        55 m_count = count;
        56 m_in_file = new char[strlen(input_file) + 1];
        57 strcpy(m_in_file, input_file);
        58 m_out_file = new char[strlen(out_file) + 1];
        59 strcpy(m_out_file, out_file);
        60 }
        61 virtual ~ExternSort()
        62 {
        63 delete [] m_in_file;
        64 delete [] m_out_file;
        65 }
        66
        67 private:
        68 int m_count; //数组长度
        69 char *m_in_file; //输入文件的路径
        70 char *m_out_file; //输出文件的路径
        71 // int k;//归并数,此数必须要内排序之后才能得到,所以下面的ls和b都只能定义为指针
        72 // LoserTree ls;//定义成为指针
        73 // External b;//定义成为指针,在成员函数中可以把它当成数组使用
        74 //int External[k];
        75 protected:
        76 int read_data(FILE* f, int a[], int n)
        77 {
        78 int i = 0;
        79 while(i < n && (fscanf(f, "%d", &a[i]) != EOF)) i++;
        80 printf("read:%d integer ", i);
        81 return i;
        82 }
        83 void write_data(FILE* f, int a[], int n)
        84 {
        85 for(int i = 0; i < n; ++i)
        86 fprintf(f, "%d ", a[i]);
        87 //fprintf(f,"%d",MAX);//在最后写上一个最大值
        88 }
        89 char* temp_filename(int index)
        90 {
        91 char *tempfile = new char[100];
        92 sprintf(tempfile, "temp%d.txt", index);
        93 return tempfile;
        94 }
        95 static int cmp_int(const void *a, const void *b)
        96 {
        97 return *(int*)a - *(int*)b;
        98 }
        99
        100 int memory_sort()
        101 {
        102 FILE* fin = fopen(m_in_file, "rt");
        103 int n = 0, file_count = 0;
        104 int *array = new int[m_count];
        105
        106 //每读入m_count个整数就在内存中做一次排序,并写入临时文件
        107 while(( n = read_data(fin, array, m_count)) > 0)
        108 {
        109 qsort(array, n, sizeof(int), cmp_int);
        110 //这里,调用了库函数阿,在第四节的c实现里,不再调用qsort。
        111 char *fileName = temp_filename(file_count++);
        112 FILE *tempFile = fopen(fileName, "w");
        113 free(fileName);
        114 write_data(tempFile, array, n);
        115 fclose(tempFile);
        116 }
        117
        118 delete [] array;
        119 fclose(fin);
        120
        121 return file_count;
        122 }
        123 /*
        124 void Adjust(int s)
        125 {
        126 int t=(s+k)/2;
        127 while(t>0)
        128 {
        129 if(b[s]>b[ls[t]])//如果失败,则失败者位置s留下,s指向新的胜利者
        130 {
        131 int tmp=s;
        132 s=ls[t];
        133 ls[t]=tmp;
        134 }
        135 t=t/2;
        136 }
        137 ls[0]=s;
        138 }
        139
        140 void CreateLoserTree()
        141 {
        142 b[k]=MIN;//额外的存储一个最小值
        143 for(int i=0;i<k;i++)ls[i]=k;//先初始化为指向最小值,这样后面的调整才是正确的
        144 //这样能保证非叶子节点都是子树中的“二把手”
        145 for(i=k-1;i>=0;i--)
        146 Adjust(i);//依次从b[k-1],b[k-2]...b[0]出发调整败者树
        147 }
        148
        149 void K_Merge()
        150 {//利用败者数把k个输入归并段归并到输出段中
        151 //b中前k个变量存放k个输入段中当前记录的元素
        152 //归并临时文件
        153 FILE *fout = fopen(m_out_file, "wt");
        154 FILE* *farray = new FILE*[k];
        155 int i;
        156 for(i = 0; i < k; ++i) //打开所有k路输入文件
        157 {
        158 char* fileName = temp_filename(i);
        159 farray[i] = fopen(fileName, "rt");
        160 free(fileName);
        161 }
        162
        163 // int *data = new int[file_count];//存储每个文件当前的一个数字
        164 //bool *hasNext = new bool[k];//标记文件是否读完
        165 // memset(data, 0, sizeof(int) * file_count);
        166 // memset(hasNext, 1, sizeof(bool) * file_count);
        167
        168 for(i = 0; i < k; ++i) //初始读取
        169 {
        170 if(fscanf(farray[i], "%d", &b[i]) == EOF)//读每个文件的第一个数到data数组
        171 // hasNext[i] = false;
        172 {
        173 printf("there is no %d file to merge!");
        174 return;
        175 }
        176 }
        177 // for(int i=0;i<k;i++)input(b[i]);
        178
        179 CreateLoserTree();
        180 int q;
        181 while(b[ls[0]]!=MAX)//
        182 {
        183 q=ls[0];//q用来存储b中最小值的位置,同时也对应一路文件
        184 //output(q);
        185 fprintf(fout,"%d ",b[q]);
        186 //input(b[q],q);
        187 fscanf(farray[q],"%d",&b[q]);
        188 Adjust(q);
        189 }
        190 //output(ls[0]);
        191 fprintf(fout,"%d ",b[ls[0]]);
        192 //delete [] hasNext;
        193 //delete [] data;
        194
        195 for(i = 0; i < k; ++i) //清理工作
        196 {
        197 fclose(farray[i]);
        198 }
        199 delete [] farray;
        200 fclose(fout);
        201 }
        202 */
        203 void merge_sort(int file_count)
        204 {
        205 if(file_count <= 0) return;
        206
        207 //归并临时文件
        208 FILE *fout = fopen(m_out_file, "wt");
        209 FILE* *farray = new FILE*[file_count];
        210 int i;
        211 for(i = 0; i < file_count; ++i)
        212 {
        213 char* fileName = temp_filename(i);
        214 farray[i] = fopen(fileName, "rt");
        215 free(fileName);
        216 }
        217
        218 int *data = new int[file_count];//存储每个文件当前的一个数字
        219 bool *hasNext = new bool[file_count];//标记文件是否读完
        220 memset(data, 0, sizeof(int) * file_count);
        221 memset(hasNext, 1, sizeof(bool) * file_count);
        222
        223 for(i = 0; i < file_count; ++i) //初始读取
        224 {
        225 if(fscanf(farray[i], "%d", &data[i]) == EOF)//读每个文件的第一个数到data数组
        226 hasNext[i] = false;
        227 }
        228
        229 while(true) //循环读取和输出,选择最小数的方法是简单遍历选择法
        230 {
        231 //求data中可用的最小的数字,并记录对应文件的索引
        232 int min = data[0];
        233 int j = 0;
        234
        235 while (j < file_count && !hasNext[j]) //顺序跳过已读取完毕的文件
        236 j++;
        237
        238 if (j >= file_count) //没有可取的数字,终止归并
        239 break;
        240
        241
        242 for(i = j +1; i < file_count; ++i) //选择最小数,这里应该是i=j吧!但结果是一样的!
        243 {
        244 if(hasNext[i] && min > data[i])
        245 {
        246 min = data[i];
        247 j = i;
        248 }
        249 }
        250
        251 if(fscanf(farray[j], "%d", &data[j]) == EOF) //读取文件的下一个元素
        252 hasNext[j] = false;
        253 fprintf(fout, "%d ", min);
        254
        255 }
        256
        257 delete [] hasNext;
        258 delete [] data;
        259
        260 for(i = 0; i < file_count; ++i)
        261 {
        262 fclose(farray[i]);
        263 }
        264 delete [] farray;
        265 fclose(fout);
        266 }
        267
        268 };
        269
        270 #endif
        271
        272
        273 //测试主函数文件
        274 /*
        275 * 大文件排序
        276 * 数据不能一次性全部装入内存
        277 * 排序文件里有多个整数,整数之间用空格隔开
        278 */
        279
        280 const unsigned int count = 10000000; // 文件里数据的行数
        281 const unsigned int number_to_sort = 100000; //在内存中一次排序的数量
        282 const char *unsort_file = "unsort_data.txt"; //原始未排序的文件名
        283 const char *sort_file = "sort_data.txt"; //已排序的文件名
        284 void init_data(unsigned int num); //随机生成数据文件
        285
        286 int main(int argc, char* *argv)
        287 {
        288 srand(time(NULL));
        289 init_data(count);
        290 ExternSort extSort(unsort_file, sort_file, number_to_sort);
        291 extSort.sort();
        292 system("pause");
        293 return 0;
        294 }
        295
        296 void init_data(unsigned int num)
        297 {
        298 FILE* f = fopen(unsort_file, "wt");
        299 for(int i = 0; i < num; ++i)
        300 fprintf(f, "%d ", rand());
        301 fclose(f);
        302 }

        4、测试分析:

        先在分别测试上述两个代码,第一组测试的参数如下:

            const unsigned int count = 10000000; // 待排序文件里数据的个数  (JULY的源文件中为文件里数据的行数,有误)
            const unsigned int number_to_sort = 1000000; //在内存中一次排序的数量 
            const char *unsort_file = "unsort_data.txt"; //原始未排序的文件名 
            const char *sort_file = "sort_data.txt"; //已排序的文件名 
            void init_data(unsigned int num); //随机生成数据文件 

        关键是前两行,一个是待排序数据的总的个数10000000,一个是每次在内存中排序的个数1000000(也就是划分后各个小文件中数据的大小)。于是程序为10路归并排序。

        JULY原版代码的运行结果:

        败者树版本代码运行结果:

        由以上可知,利用了败者树的程序运行速度略高于直接选择版本程序,但是差别不是很大,对于不同的机器来说,差别可能在1到2秒之内,甚至没有差别,读者可以自行测试。

        接下来,我们进行第二组测试,测试数据如下

            const unsigned int count = 10000000; // 待排序文件里数据的个数  (JULY的源文件中为文件里数据的行数,有误)
            const unsigned int number_to_sort = 100000; //在内存中一次排序的数量  (注意这里改为十万)
            const char *unsort_file = "unsort_data.txt"; //原始未排序的文件名 
            const char *sort_file = "sort_data.txt"; //已排序的文件名 
            void init_data(unsigned int num); //随机生成数据文件 

        关键是前两行,一个是待排序数据的总的个数10000000,一个是每次在内存中排序的个数100000(也就是划分后各个小文件中数据的大小)。于是程序为100路归并排序。

        JULY原版代码的运行结果:

        败者树版本代码运行结果:

        由以上可知,两者的差别明显地体现了出来,败者树版本程序显然快于直接选择版本。

        另外还可以看出,10路和100路对于败者树版本来说,耗时差不多,说明败者树版本随路数的增加,耗时的增加相对较缓;而对于直接选择版本来说,耗时会随着路数的增加而增加,至于是线性的还是指数型的读者可以自行验证~~。

        5、结论

            当多路归并的路数比较小时,败者树的优势体现不出来,但是当路数达到一定规模时,败者树可以显著地减少排序时间,当然,由于败者树只作用于内存的最小关键字选择,所以直接提高的也只是内存的速度而已。但是别忘了,外排时所需读写外存的次数是和归并的次数成正比的,路数越多,归并的次数越少,也就可以间接减少外存读写次数了,所以说,败者树的优势是相当强大的~~

        与学习海量数据处理同学共勉之~!
        标签: 海量数据处理 编程珠玑 外部排序
        绿色通道: 好文要顶 关注我 收藏该文与我联系
        harryshayne
        关注 - 0
        粉丝 - 2
        +加关注
        1
        0
        (请您对文章做出评价)
        « 上一篇:windows程序设计之STRPROG学习总结
        posted @ 2011-07-02 11:52 harryshayne 阅读(3111) 评论(1) 编辑 收藏
        发表评论
         
        #1楼 2012-07-05 10:16 | ice2000feng 
        用败者树的算法不正确,排序完后,数据丢失了一些。
  • 相关阅读:
    在线教程
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---46
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---45
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---44
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---42
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---43
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---41
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---40
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---37
    《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---36
  • 原文地址:https://www.cnblogs.com/lexus/p/3313543.html
Copyright © 2011-2022 走看看