多路平衡归并排序(胜者树、败者树)算法详解
但是经过计算得知,如果毫无限度地增加 k 值,虽然会减少读写外存数据的次数,但会增加内部归并的时间,得不偿失。
例如在上节中,对于 10 个临时文件,当采用 2-路平衡归并时,若每次从 2 个文件中想得到一个最小值时只需比较 1 次;而采用 5-路平衡归并时,若每次从 5 个文件中想得到一个最小值就需要比较 4 次。以上仅仅是得到一个最小值记录,如要得到整个临时文件,其耗费的时间就会相差很大。
为了避免在增加 k 值的过程中影响内部归并的效率,在进行 k-路归并时可以使用“败者树”来实现,该方法在增加 k 值时不会影响其内部归并的效率。
败者树实现内部归并
败者树是树形选择排序的一种变形,本身是一棵完全二叉树。在树形选择排序一节中,对于无序表
{49,38,65,97,76,13,27,49}创建的完全二叉树如图 1 所示,构建此树的目的是选出无序表中的最小值。这棵树与败者树正好相反,是一棵“胜者树”。因为树中每个非终端结点(除叶子结点之外的其它结点)中的值都表示的是左右孩子相比较后的较小值(谁最小即为胜者)。例如叶子结点 49 和 38 相对比,由于 38 更小,所以其双亲结点中的值保留的是胜者 38。然后用 38 去继续同上层去比较,一直比较到树的根结点。

图 1 胜者树
而败者树恰好相反,其双亲结点存储的是左右孩子比较之后的失败者,而胜利者则继续同其它的胜者去比较。
例如还是图 1 中,叶子结点 49 和 38 比较,38 更小,所以 38 是胜利者,49 为失败者,但由于是败者树,所以其双亲结点存储的应该是 49;同样,叶子结点 65 和 97 比较,其双亲结点中存储的是 97 ,而 65 则用来同 38 进行比较,65 会存储到 97 和 49 的双亲结点的位置,38 继续做后续的胜者比较,依次类推。
胜者树和败者树的区别就是:胜者树中的非终端结点中存储的是胜利的一方;而败者树中的非终端结点存储的是失败的一方。而在比较过程中,都是拿胜者去比较。

当最终胜者判断完成后,只需要更新叶子结点 b3 的值,即导入关键字 15,然后让该结点不断同其双亲结点所表示的关键字进行比较,败者留在双亲结点中,胜者继续向上比较。
例如,叶子结点 15 先同其双亲结点 ls[4] 中表示的 b4 中的 12 进行比较,12 为胜利者,则 ls[4] 改为 15,然后 12 继续同 ls[2] 中表示的 10 做比较,10 为胜者,然后 10 继续同其双亲结点 ls[1] 表示的 b1(关键字 9)作比较,最终 9 为胜者。整个过程如下图所示:

注意:为了防止在归并过程中某个归并段变为空,处理的办法为:可以在每个归并段附加一个关键字为值的记录。这样当某一时刻选出的冠军为值时,表明 5 个归并段已全部归并完成。(因为只要还有记录,最终的胜者就不可能是附加的值)
败者树内部归并的具体实现
#include <stdio.h>
#define k 5
#define MAXKEY 10000
#define MINKEY -1
typedef int LoserTree[k];//表示非终端结点,由于是完全二叉树,所以可以使用一维数组来表示
typedef struct {
int key;
}ExNode,External[k+1];
External b;//表示败者树的叶子结点
//a0-a4为5个初始归并段
int a0[]={10,15,16};
int a1[]={9,18,20};
int a2[]={20,22,40};
int a3[]={6,15,25};
int a4[]={12,37,48};
//t0-t4用于模拟从初始归并段中读入记录时使用
int t0=0,t1=0,t2=0,t3=0,t4=0;
//沿从叶子结点b[s]到根结点ls[0]的路径调整败者树
void Adjust(LoserTree ls,int s){
int t=(s+k)/2;
while (t>0) {
//判断每一个叶子结点同其双亲结点中记录的败者的值相比较,调整败者的值,其中 s 一直表示的都是胜者
if (b[s].key>b[ls[t]].key) {
int swap=s;
s=ls[t];
ls[t]=swap;
}
t=t/2;
}
//最终将胜者的值赋给 ls[0]
ls[0]=s;
}
//创建败者树
void CreateLoserTree(LoserTree ls){
b[k].key=MINKEY;
//设置ls数组中败者的初始值
for (int i=0; i<k; i++) {
ls[i]=k;
}
//对于每一个叶子结点,调整败者树中非终端结点中记录败者的值
for (int i=k-1; i>=0; i--) {
Adjust(ls, i);
}
}
//模拟从外存向内存读入初始归并段中的每一小部分
void input(int i){
switch (i) {
case 0:
if (t0<3) {
b[i].key=a0[t0];
t0++;
}else{
b[i].key=MAXKEY;
}
break;
case 1:
if (t1<3) {
b[i].key=a1[t1];
t1++;
}else{
b[i].key=MAXKEY;
}
break;
case 2:
if (t2<3) {
b[i].key=a2[t2];
t2++;
}else{
b[i].key=MAXKEY;
}
break;
case 3:
if (t3<3) {
b[i].key=a3[t3];
t3++;
}else{
b[i].key=MAXKEY;
}
break;
case 4:
if (t4<3) {
b[i].key=a4[t4];
t4++;
}else{
b[i].key=MAXKEY;
}
break;
default:
break;
}
}
//败者树的建立及内部归并
void K_Merge(LoserTree ls){
//模拟从外存中的5个初始归并段中向内存调取数据
for (int i=0; i<=k; i++) {
input(i);
}
//创建败者树
CreateLoserTree(ls);
//最终的胜者存储在 is[0]中,当其值为 MAXKEY时,证明5个临时文件归并结束
while (b[ls[0]].key!=MAXKEY) {
//输出过程模拟向外存写的操作
printf("%d ",b[ls[0]].key);
//继续读入后续的记录
input(ls[0]);
//根据新读入的记录的关键字的值,重新调整败者树,找出最终的胜者
Adjust(ls,ls[0]);
}
}
int main(int argc, const char * argv[]) {
LoserTree ls;
K_Merge(ls);
return 0;
}
运行结果:
6 9 10 12 15 15 16 18 20 20 22 25 37 40 48
总结
本节介绍了通过使用败者树来实现增加 k-路归并的规模来提高外部排序的整体效率。但是对于 k 值得选择也并不是一味地越大越好,而是需要综考虑选择一个适的 k 值。
- 随机文章
- 从化马尔代夫视频(探索从化马尔代夫起飞的惊险骑行之旅)
- 国足 不敌 马尔代夫(国足遭马尔代夫逆转败北)
- 女人喜欢马尔代夫(女性最喜爱的旅游目的地——马尔代夫)
- 去香港去马尔代夫(从香港到马尔代夫,探访极致海岛风光)
- 上海到马尔代夫要(上海始发到马尔代夫的航线及预订攻略)
- 中国马尔代夫别墅(中国马尔代夫私家别墅,尽享热带风情)
- 安宁马尔代夫视频(赏心悦目!领略马尔代夫梦幻美景影像)
- 广东马尔代夫游泳(广东清远池尾河打造马尔代夫游泳胜地)
- 云南马尔代夫现状(云南一马尔代夫景点的发展及现状分析)
- 伊朗对战马尔代夫(伊朗和马尔代夫进行激烈的比赛)
- 微商马尔代夫日常(马尔代夫微商生活日常,简单轻松赚钱)
- 缅甸 马尔代夫 亚洲杯(缅甸与马尔代夫确认参加2023年亚洲杯)
- 横屏马尔代夫视频(惊艳横屏!马尔代夫视频带你虚拟旅行)
- 海宁马尔代夫视频(海宁小伙拍下夫妻在马尔代夫浪漫画面)
- 写乐 马尔代夫(探秘马尔代夫——珍贵海洋生态的天堂)
- 类似马尔代夫岛屿(海上天堂:像马尔代夫一样的岛屿体验)
- 左州 马尔代夫(左州控股入主马尔代夫度假酒店)
- 柳江马尔代夫农庄(柳江小城马尔代夫农庄:度假的好去处)
- 逃脱游戏马尔代夫(马尔代夫逃脱游戏,你能成功逃脱吗?)
- 苏然 马尔代夫(苏然在马尔代夫的精彩之旅)
- 长春马尔代夫海岛(长春市民可以赴马尔代夫海岛免费游玩)
- 阳泉马尔代夫旅游(阳泉市民亲测:马尔代夫旅游实用指南)
- 西宁马尔代夫风景(西宁仿佛马尔代夫,碧波荡漾醉美风光)
- 马尔代夫亲子优惠(马尔代夫亲子游优惠活动,快来享受吧)
- 长春马尔代夫报价(长春至马尔代夫旅游报价,尽在这里!)
- 马尔代夫丽贝尔岛(马尔代夫美丽的丽贝尔岛等着你来探索)
- 只要 马尔代夫(重温梦幻之旅:马尔代夫,海与岛的完美结合)
- 长治马尔代夫越野(长治摩尔多瓦拓荒车探秘马尔代夫岛屿)
- 马尔代夫公园门票(马尔代夫公园门票价格和购买方式一览)
- 辉县马尔代夫地址(辉县有家马尔代夫酒店,让你身临其境)
