胜者树的简单玩法

本文讨论了外部排序中使用k路归并时如何通过优化归并过程,特别是引入胜者树算法来减少内存和外存间的交互次数。通过分析k与归并轮数L之间的单调关系,阐述了胜者树如何实现稳定排序的同时减少比较次数,进而提高排序效率。


      设一次外部排序采用k路归并,元素个数为n,需要进行L轮归并:

                       

即一颗k叉树的高度,每轮归并都需要访问一次磁盘(外存),降低了系统的性能。因此,我们要想办法降低内存和外存间的交互次数。

假设n是常量,那么kL的单调关系呢?

k为自变量对L求导:


显然在k的定义域内<0恒成立,因此可知L(k)k的定义域[2,+)内单调递减,即归并路数越大,内存和外存间的访问次数也越少。

那么问题来了,是不是k越大越好?

当然不是,我们知道按照土一点的办法,每次k个数据段间归并都需要进行k-1次比较才能得出一个最小值,放到新的数据段中(对2路归并同样适用),k越大,需要比较的次数也越大。我们知道,使用胜者树或者败者树可以大大减少比较的次数,从k-1次降到次,下面来介绍一种简单的胜者树算法。

胜者树是一棵二叉树,每个叶子结点存放的是各个归并段当前的最小值,兄弟结点之间互相比较,值更小的结点即为胜者,需要赋给父结点,然后再往上进行比较。最终的根节点即为冠军结点,是这次归并的最小值,放到下一轮归并的数据段中。

       

    

 

由于胜者树是一棵完全二叉树,因此可以利用一维数组存放。

a[MAX_NUM] = {0};

先初始化空树的叶子结点:

 

for (int i = k - 1;i < 2k - 1; i++)
{
    /*把每个归并段的首个元素(最小值)放到数组a[]内的k -1 ~ 2k - 2下标*/
    /*略*/
}

第一轮比较,即初始化胜者树,同时决出第一个冠军结点,可以采用递归的方式:

m = 2k - 2;
while(m >= 2)
{
    /*兄弟节点a[m - 1]和a[m]之间胜者的值赋给他们的父结点a[(m - 2) / 2]*/
    a[(m - 2) / 2] = a[m - 1] < a[m]?:a[m - 1] : a[m];
    m -= 2;
}

第一轮需要执行k-1次比较

 

后续每次比较只需要替换上一次冠军结点所在的叶子结点的值,然后在该结点所在路径开始进行比较即可,其他路径上的比赛结果无需改变。

m = m';//初始化m为上一次比赛的冠军结点的值所在叶子结点的数组下标
while(m >= 2)
{
    /*兄弟节点a[m - 1]和a[m]之间胜者的值赋给他们的父结点a[(m - 2) / 2]*/
    a[(m - 2) / 2] = a[m - 1] < a[m]?:a[m - 1] : a[m];
    m = (m - 2) / 2;//下一次直接跳到父结点进行比较
}

后续的每一轮需要执行次比较

胜者树排序是一种稳定的排序算法。

很多文章强调败者树优于胜者树,因为败者树结点(除了叶子结点)只保存上一次比较失败的子结点的值,下一次比较的时候新结点只需要和父结点比较即可,无需通过父结点获取兄弟结点的值后再进行比较。窃以为使用一维数组存放胜者树的结点值可以解决这个问题,兄弟结点之间可以通过取数组下标进行直接比较,败者树也就没有优势可言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值