现在的位置: 首页 > 综合 > 正文

多路归并排序

2011年04月08日 ⁄ 综合 ⁄ 共 3986字 ⁄ 字号 评论关闭

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

抱歉!评论已关闭.