实现多路平衡归并算法

对于外部排序算法来说,直接影响算法效率的因素为读写外存的次数,即次数越多,算法效率越低。若想提高算法的效率,即减少算法运行过程中读写外存的次数,可以增加 k–路平衡归并中的 k 值。

经过计算得知,如果毫无限度地增加 k 值,虽然会减少读写外存数据的次数,但会增加内部归并的时间,得不偿失。

对于 10 个临时文件,当采用 2-路平衡归并时,若每次从 2 个文件中想得到一个最小值时只需比较 1 次;而采用 5-路平衡归并时,若每次从 5 个文件中想得到一个最小值就需要比较 4 次。以上仅仅是得到一个最小值记录,如要得到整个临时文件,其耗费的时间就会相差很大。

为避免在增加 k 值的过程中影响内部归并的效率,在进行 k-路归并时可使用败者树来实现,该方法在增加 k 值时不会影响其内部归并的效率。

败者树实现内部归并

败者树是树形选择排序的一种变形,本身是一棵完全二叉树。

对于无序表{49,38,65,97,76,13,27,49}创建的完全二叉树如图1所示,构建此树的目的是选出无序表中的最小值。

                                                                          图 1 胜者树

这棵树与败者树正好相反,是一棵胜者树。因为树中每个非终端结点(除叶子结点之外的其它结点)中的值都表示的是左右孩子相比较后的较小值(谁最小即为胜者)。例如叶子结点 49 和 38 相对比,由于 38 更小,所以其双亲结点中的值保留的是胜者 38。然后用 38 去继续同上层去比较,一直比较到树的根结点。

而败者树恰好相反,其双亲结点存储的是左右孩子比较之后的失败者,而胜利者则继续同其它的胜者去比较。

例如图 1 中,叶子结点 49 和 38 比较,38 更小,所以 38 是胜利者,49 为失败者,但由于是败者树,所以其双亲结点存储的应该是 49;同样,叶子结点 65 和 97 比较,其双亲结点中存储的是 97 ,而 65 则用来同 38 进行比较,65 会存储到 97 和 49 的双亲结点的位置,38 继续做后续的胜者比较,依次类推。

胜者树和败者树的区别就是:胜者树中的非终端结点中存储的是胜利的一方;而败者树中的非终端结点存储的是失败的一方。而在比较过程中,都是拿胜者去比较。

 

                                                 图 2 败者树

如图 2 所示为一棵 5-路归并的败者树,其中 b0—b4 为树的叶子结点,分别为 5 个归并段中存储的记录的关键字。 ls 为一维数组,表示的是非终端结点,其中存储的数值表示第几归并段(例如 b0 为第 0 个归并段)。ls[0] 中存储的为最终的胜者,表示当前第 3 归并段中的关键字最小。

当最终胜者判断完成后,只需要更新叶子结点 b3 的值,即导入关键字 15,然后让该结点不断同其双亲结点所表示的关键字进行比较,败者留在双亲结点中,胜者继续向上比较。

例如,叶子结点 15 先同其双亲结点 ls[4] 中表示的 b4 中的 12 进行比较,12 为胜利者,则 ls[4] 改为 15,然后 12 继续同 ls[2] 中表示的 10 做比较,10 为胜者,然后 10 继续同其双亲结点 ls[1] 表示的 b1(关键字 9)作比较,最终 9 为胜者。整个过程如下图所示:

 

注意:

为了防止在归并过程中某个归并段变为空,处理的办法为:可以在每个归并段最后附加一个关键字为最大值的记录。这样当某一时刻选出的冠军为最大值时,表明 5 个归并段已全部归并完成。(因为只要还有记录,最终的胜者就不可能是附加的最大值)。

败者树:

败者树是胜者树的一种变体。在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者(数值大的),需要加一个结点来记录整个比赛的胜利者。采用败者树可以简化重构的过程。

/**
*    实验题目:
*        实现多路平衡归并算法
*    实验目的:
*        领会外排序中多路平衡归并的执行过程和算法设计
*    实验内容:
*        编写程序,模拟利用败者树实现5路归并算法的过程以求解以下问题:
*    设有5个文件中记录关键字如下:
*    F0:{17, 21, ∞} F1:{5, 44, ∞}   F2:{10, 12, ∞}  F3{29, 32, ∞}   F4{15, 56, ∞}
*    要求将其归并为一个有序段并输出。假设这些输入文件数据存放在内存中,输出
*    结果直接在屏幕上显示。
*/

#include <stdio.h>

#define MAX_SIZE    20                          //  每个文件中的最多记录
#define K           5                           //  5路平衡归并
#define MAX_KEY     32767                       //  最大关键字值∞
#define MIN_KEY     -32768                      //  最小关键字值-∞

typedef int info_type;
typedef int key_type;
typedef struct
{
    key_type key;                               //  关键字项
    info_type other_info;                       //  其他数据项,具体类型在主程序中定义
}rec_type;                                      //  文件中记录的类型

typedef struct
{
    rec_type recs[MAX_SIZE];
    int cur_rec;
}file_type;                                     //  模拟的文件类型

typedef int loser_tree;                         //  败者树为loser_tree[K]

/*------------------------以下为全局变量------------------------*/
rec_type b[K];                                  //  b中存放各段中取出的当前记录
file_type F[K];                                 //  存放文件记录的数组


/*-----------------初始化存放文件记录的数组F---------------------*/
static void initial(void)
{
    int i;

    //  第1个初始文件
    F[0].recs[0].key = 17;
    F[0].recs[1].key = 21;
    F[0].recs[2].key = MAX_KEY;

    //  第2个初始文件
    F[1].recs[0].key = 5;
    F[1].recs[1].key = 44;
    F[1].recs[2].key = MAX_KEY;

    //  第3个初始文件
    F[2].recs[0].key = 10;
    F[2].recs[1].key = 12严蔚敏,;
    F[2].recs[2].key = MAX_KEY;

    //  第4个初始文件
    F[3].recs[0].key = 29;
    F[3].recs[1].key = 32;
    F[3].recs[2].key = MAX_KEY;

    //  第5个初始文件
    F[4].recs[0].key = 15;
    F[4].recs[1].key = 56;
    F[4].recs[2].key = MAX_KEY;

    //  5个初始文件,当前读记录号为-1
    for(i = 0; i < K; i++)
        F[i].cur_rec = -1;
}

/*--------------从F[i]文件中读一个记录到b[i]中---------------*/
static void input(int i, int &key)
{
    F[i].cur_rec++;
    key = F[i].recs[F[i].cur_rec].key;          // F[0].recs[0].key,F[1].recs[0].key,...,F[4].recs[0].key
}

static int cnt = 0;
/*--------------沿从叶子结点b[s]到根结点ls[0]=5的路径调整败者树--------------*/
// ls为一维数组,表示的是非终端结点,其存储的数值表示第几归并段,例如b[0]为第0个归并段
static void adjust(loser_tree ls[K], int s)     //  s=4,3,2,1,0
{
    int i;
    int t;

    t = (s + K) / 2;                            //  ls[t]是b[s]的双亲结点
    printf("    调整如下:\n");
    printf("\tt = %d, 沿从叶子结点b[%d] = %2d到根结点ls[0]的路径调整败者树\n", t, s, b[s]);
    while(t > 0)
    {
        printf("\tb[%d].key = %d, t = %d, ls[%d] = %d, b[ls[%d]].key = %d\n", s, b[s].key, t, t, ls[t], t, b[ls[t]].key);
        if(b[s].key > b[ls[t]].key)
        {
            i = s;                              //  i保存当前第几个归并段
            s = ls[t];                          //  s指示新的胜者,数值小的获胜
            ls[t] = i;                          //  双亲结点ls[t]存放败者,表示第几个归并段
            printf("\ti = %d, s = %d, ls[%d] = %d\n", i, s, t, ls[t]);
        }
        t = t / 2;
    }
    ls[0] = s;                                  //  根结点存放第几个归并段
    printf("\tls[0] = %d\n", ls[0]);
}

/*--------------创建败者树ls--------------*/
static void create_loser_tree(loser_tree ls[K])
{
    int s;

    b[K].key = MIN_KEY;
    for(s = 0; s < K; s++)                      //  设置ls中"败者"的初值,全部为最小关键字MIN_KEY的段号K=5
        ls[s] = K;

    for(s = K - 1; s >= 0; --s)                 //  依次从b[4],b[3],...,b[0]出发调整败者
        adjust(ls, s);
}

/*--------------显示败者树----------------*/
static void display(loser_tree ls[K])
{
    int i;

    printf("(%d)败者树(归并段,关键字):\n", ++cnt);
    for(i = 0; i < K; i++)
    {
        if(b[ls[i]].key == MAX_KEY)
        {
            printf("(ls[%d] = %d, key = ∞) ", i, ls[i]);
        }
        else if(b[ls[i]].key == MIN_KEY)
        {
            printf("(ls[%d] = %d, key = -∞) ", i, ls[i]);
        }
        else
        {
            printf("(ls[%d] = %d, key = %d) ", i, ls[i], b[ls[i]].key);
        }
    }
    printf("\n");
}

/*--------------输出F[q]中的当前记录--------------*/
static void output(int q)
{
    printf("输出F[%d]的关键字%d\n", q, F[q].recs[F[q].cur_rec].key);
}

/*--------------利用败者树ls进行k路归并到输出-----------------*/
static void k_merge(loser_tree ls[K])
{
    int i;                                  //  循环变量
    int rno;                                //  rno指示当前最小关键字所在归并段

    for(i = 0; i < K; i++)                  //  分别从k个输入归并段读入该段当前第一个记录的关键字到b
    {
        input(i, b[i].key);
        printf("从文件读记录到b->b[%d] = %2d\n", i, b[i].key);
    }
    create_loser_tree(ls);                  //  创建败者树ls,选得最小关键字为b[ls[0]].key
    display(ls);

    while(b[ls[0]].key != MAX_KEY)
    {
        rno = ls[0];                          //  rno指示当前最小关键字所在归并段
        output(rno);                          //  将编号为rno的归并段中当前关键字为b[rno].key的记录输出
        input(rno, b[rno].key);               //  从编号为rno的输入归并段中读入下一个记录的关键字
        printf("从输入归并段%d读记录关键字%d\n", rno, b[rno].key);
        if(b[rno].key == MAX_KEY)
            printf("从F[%d]中添加关键字∞并调整\n", rno);
        else
            printf("从F[%d]中添加关键字%d并调整\n", rno, b[rno].key);
        adjust(ls, rno);                      //  调整败者树,选择新的最小关键字
        display(ls);                          //  输出败者树
    }
}

int main(void)
{
    loser_tree ls[K];
    printf("F0:{17, 21, ∞} F1:{5, 44, ∞} F2:{10, 12, ∞} F3{29, 32, ∞} F4{15, 56, ∞}\n");

    initial();
    k_merge(ls);

    return 0;
}
 

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/87694592
今日推荐