二路和多路归并排序

博客介绍了二路和多路归并排序的原理,特别是如何在外排序场景下,利用败者树优化k路归并排序的过程。通过建立败者树,可以在O(k)时间内找到第一个胜者,并以O(lgk)的时间复杂度决定后续胜者,从而实现大规模有序数据的高效合并。同时,文章也讨论了如何调整算法以实现降序排序。

先通俗地说一下归并排序。打个比方,桌子上有两堆扑克牌,每堆扑克牌都是已经排好序的(不妨假设是升序)。我们从两堆扑克牌中各取最上面的一张(取的两张牌显然分别是两堆牌中最小的 ),比较大小,把最小的一张拿在手里,然后从“胜出”的牌所在的牌堆中再取一张。重复上面的过程。。。如果有一个牌堆被取完了,那么就把另一堆一股脑搬过来。这时候,你手中的牌就是升序的。换句话说,通过上面的算法,你成功把两个各为升序的牌堆合成了一个升序的牌堆——二合一。可以证明,该算法的复杂度是O(nlgn)。

上面所说的其实就是一个二路归并排序。显然,如果把k堆牌合成一堆,那就是k路归并排序。

考虑这样一个问题,我这里有k个文件,里面各自存储着已经排好序(不妨假设是升序)的,并且序相同的(要么都是升序要么都是降序)的整数。文件里的数据量很大,全部一股脑的装到内存里然后排序在输出是不现实的。这时候,就需要进行外排序,这时数据不会一次性全部进入内存。

在看接下来的内容之前,强烈建议读者在纸上演示一下,更能加深理解。

我们可以先建立一个败者树。所谓败者,就是在两两比较中不被选中的数,比方说我想要排成升序,那么两数之间较大的数就不会被选中,那它就是败者。b[i]表示从第i个文件中按顺序取出的数(类比一下牌堆。。),这作为败者树的叶节点。然后,两子节点的父节点存储的是败者的所在文件编号,胜者就晋级去更高一层比赛,那么冠军就应该是我们要的数(通过b[LoserTree[0]]得到)。接下来,每个节点都记录了每场比赛的败者,这些信息将成为优化的关键。我们从胜者所在的文件里再取一个数,存储在b[s]中,然后我们只需要更新从叶节点s一路更新到根节点,不需要理会其他节点。因为其他节点所代表的比赛结果和决定接下来的胜者没有关系。

这样一来,我们在O(k)的时间内决定第一个胜者,然后不断地用O(lgk)的时间决定下一个胜者。

/*
以下程序执行k路归并排序,最终结果为升序
*/
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
#define k 3  //归并排序的路数
#define FLAG 100
/*
表示文件结束的“哨兵值”,每个输入文件的末尾都有它,对于升序排序,他应该是比
所有待排的数都大,这样只要还有数没排,它总是个败者。那如果他晋级成了冠军呢?
*/
#define KEY -1
/*
KEY用于初始化败者树中的败者,这应该是一个比待排序文件中任何数都
小的数,保证第一轮调整能顺利进行(使每个与他比较的数都成为败者)。
由于不将其输出,这个冠军被“忽略”。
*/
FILE *fp[k+1];//fp[0]到fp[k-1]为输入文件(小牌堆),fp[k]为输出文件(最终的总牌堆)。
int LoserTree[k];  //败者树,在这里用顺序存储结构。存储的是每个败者所在的文件编号
int b[k+1];  //我们会将KEY放在b[k]中,b数组的含义同前文
int input(int i)  //从第i个文件中取数
{
    int val;
    fscanf(fp[i],"%d",&val);
    return val;
}
void output(int val)  //将val输出到输出文件中,fp[k]是指向输出文件的指针
{
    fprintf(fp[k],"%d ",val);
}
void Adjust(int s) //从叶节点s向根节点一路更新,t为父节点,LoserTree[t]存储的是败者
{
    int i,t;
    t=(s+k)/2;  //这样可以得到父节点
    while(t>0)
    {  /*s指示新的胜者,他也许不会是一成不变的*/
        if(b[s]>b[LoserTree[t]])
/*与父节点的比较实际上就是在与“擂主”比较(这个节点对应比赛的胜者被选走,败者就成了擂主)
这一比有两种情况:挑战者大于擂主,按升序挑战者就成了败者,也就是这个if语句对应的情况,
s与LoserTree[t]交换,挑战者在这个节点“待着”,原擂主成为胜者晋级(对应s),否则挑战者晋级*/
        {
            i=s;s=LoserTree[t];LoserTree[t]=i;
        }
        t=t/2;  //这样可以得到父节点
    }
    LoserTree[0]=s;  //这一趟比赛最后的冠军!!
}
void CreateLoserTree()  //创建败者树
{
    b[k]=KEY;//不懂这一步可以翻看前文预编译KEY的那一句
    for(int i=0;i<k;i++)
        LoserTree[i]=k;  //b[k]称为每场比赛的擂主
    for(int i=0;i<k;i++)
        Adjust(i);   //各路英雄前来挑战,然而胜负早已决定(不懂看前面KEY)
}
void K_Merge()  //开始k路归并排序
{
    int p;
    for(int i=0;i<k;i++)  //从各个文件里取数
        b[i]=input(i);
    CreateLoserTree();  //创建败者树
    while(b[LoserTree[0]]!=FLAG)
//哨兵成了冠军的话。。没有数需要排了,只要还有数就肯定比他强
    {
        p=LoserTree[0];
        output(b[p]);   //把冠军输出到文件
        b[p]=input(p);  //取出下一个数
        Adjust(p);   //一路往根节点更新
    }
    output(FLAG);   //关于这句我在文末会解释
}
int main()
{
    char fname[k][15],fout[15];  //存储输入文件名和输出文件名
    for(int i=0;i<k;i++)
    {
        printf("请输入第%d个输入文件名:\n",i+1);
        gets(fname[i]);
        fp[i]=fopen(fname[i],"r");  //以只读形式打开文件
    }
    printf("请输入输出文件名:\n");
    gets(fout);
    fp[k]=fopen(fout,"w");   //以只写方式打开文件
    K_Merge();         //k路归并排序
    for(int i=0;i<=k;i++)
        fclose(fp[i]);   //别忘了关闭文件
    return 0;
}
/*
在这里设输入文件名为"in1.txt","in2.txt","in3.txt",
输出文件名为"out.txt"(当然这些都有你随意制定)
其中的数据为:in1.txt:10 15 16 100
              in2.txt:9 18 20 100
              in3.txt:20 22 40 100
运行之后,输出文件中为:9 10 15 16 18 20 20 22 40 100 
*/
为什么我们最后要把FLAG对应的哨兵值也输出呢?考虑这一种情况:待排的文件数目也相当多,也许有上百万,那么我们不能这些文件一次排完,要分多次运行程序。那么这次的输出可能成为下次的输入,输入文件最后当然要有FLAG啦。。

如果是降序呢?首先FLAG的值要调整为一个足够小的数,然后KEY(第一轮比赛中注定的胜者)要调整为一个足够大的数。比赛规则要改:b[s]>b[LowerTree[0]]改为b[s]<b[LowerTree[0]]。子文件的顺序也要是降序。这样就可以进行输出为降序的k路归并排序了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值