• 多路归并排序


    下面的问题描述及相关文字参考于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;
    }
  • 相关阅读:
    不断进化中的云原生
    Helium (HNT)
    Linux上抓包工具tcpdump的使用
    docker开始收费之日,就是被抛弃之时,全面拥抱podman
    Mysql数据库的几大优势
    Google Doodle:龙年 新春快乐 龙年进步!
    夏季晨练不得早于6点 生活至上,美容至尚!
    洗脸方法帮你提升面部肤质 生活至上,美容至尚!
    午饭喝酸奶营养健康好身体 生活至上,美容至尚!
    夏天大作战,防晒油绝招 生活至上,美容至尚!
  • 原文地址:https://www.cnblogs.com/qianye/p/2788231.html
Copyright © 2020-2023  润新知