设一次外部排序采用k路归并,元素个数为n,需要进行L轮归并:
即一颗k叉树的高度,每轮归并都需要访问一次磁盘(外存),降低了系统的性能。因此,我们要想办法降低内存和外存间的交互次数。
假设n是常量,那么k和L的单调关系呢?
以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;//下一次直接跳到父结点进行比较
}后续的每一轮需要执行次比较
胜者树排序是一种稳定的排序算法。
很多文章强调败者树优于胜者树,因为败者树结点(除了叶子结点)只保存上一次比较失败的子结点的值,下一次比较的时候新结点只需要和父结点比较即可,无需通过父结点获取兄弟结点的值后再进行比较。窃以为使用一维数组存放胜者树的结点值可以解决这个问题,兄弟结点之间可以通过取数组下标进行直接比较,败者树也就没有优势可言。
本文讨论了外部排序中使用k路归并时如何通过优化归并过程,特别是引入胜者树算法来减少内存和外存间的交互次数。通过分析k与归并轮数L之间的单调关系,阐述了胜者树如何实现稳定排序的同时减少比较次数,进而提高排序效率。

7415

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



