多路归并排序

下面的问题描述及相关文字参考于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;

}

你可能感兴趣的:(归并排序)