先通俗地说一下归并排序。打个比方,桌子上有两堆扑克牌,每堆扑克牌都是已经排好序的(不妨假设是升序)。我们从两堆扑克牌中各取最上面的一张(取的两张牌显然分别是两堆牌中最小的 ),比较大小,把最小的一张拿在手里,然后从“胜出”的牌所在的牌堆中再取一张。重复上面的过程。。。如果有一个牌堆被取完了,那么就把另一堆一股脑搬过来。这时候,你手中的牌就是升序的。换句话说,通过上面的算法,你成功把两个各为升序的牌堆合成了一个升序的牌堆——二合一。可以证明,该算法的复杂度是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路归并排序了。
博客介绍了二路和多路归并排序的原理,特别是如何在外排序场景下,利用败者树优化k路归并排序的过程。通过建立败者树,可以在O(k)时间内找到第一个胜者,并以O(lgk)的时间复杂度决定后续胜者,从而实现大规模有序数据的高效合并。同时,文章也讨论了如何调整算法以实现降序排序。

7302

被折叠的 条评论
为什么被折叠?



