排序4:多路归并排序之预备:胜者树与败者树

本文探讨了多路归并排序的概念及其时间复杂度,并介绍了胜者树和败者树这两种数据结构,它们能在对数时间内找出数据集的最大或最小值,非常适合应用于多路归并排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇文章写了普通归并排序即2路归并排序,那么多路归并排序是什么?

参考上一篇文章排序4:普通归并排序,普通归并排序是把数据分为两个部分,然后每个部分再不断的分为两个新的部分,这样不断的拆分,当最终 拆分的数据集的长度小于等于2时,停止拆分进行合并排序,合并排序使用O(N)的临时空间,使达到O(N)的合并排序的时间复杂度,这样总的时间复杂度就等于:O(N * logN)

那么多路归并排序呢,是不是就是分成多路而不是2路,如下图:


比如上图是3路归并排序,总共7个数据,第一次拆分分成了长度分别为3、3、1的3个数据集,因为数据集长度已经满足小于等于3,至此就不用再拆分了,就此进行合并排序;

对于最大长度为3的数据集的合并排序,时间复杂度就不是O(N)了,因为每次获取3个数据集的当前极值,按照普通归并排序的方式,需要比较2次,所以对于3路归并排序,每一层的合并排序的时间复杂度是O(2 * N)。

同理,4路归并排序,每一层的合并排序的时间复杂度是O(3 * N),5路归并排序是O(4 * N),即M路归并排序的每一层的合并排序的时间复杂度是O((M - 1) * N),进而可知总的时间复杂度是:O(logM(N) * (M - 1) * N) = O(logN/logM * (M - 1) * N) => O(logN/logM * M * N)。

O(logN/logM * M * N,在M比较大的情况下,还是比较影响效率,因为很明显,M的线性变大的负效应,大于logM的对数变大的正效应。

有一种数据结构算法可以在对数时间复杂度logM的速度下,得到M个数的最大/小值,那么每一层的合并排序的时间复杂度就可以降为O(logM * N),这种数据结构算法就是胜者树败者树,胜者树败者树典型用于多路归并排序。


1、胜者树:

贴一个网上的图片(http://www.cnblogs.com/qianye/archive/2012/11/25/2787923.html):


对于长度为5的数据集[10,9,20,6,12],如何在O(log5)的时间复杂度内找到其最大/小值,使用胜者树。胜者树的操作有点像二叉堆

上图的意思是:

1、待找到极值的数据集,全部作为一个完全二叉树的叶子节点

2、回顾一下完全二叉树的特点:最后一个非叶子节点的索引是(总共节点数/2),每个节点i的左子节点的索引是2 * i,右子节点的索引是2 * i + 1,父节点索引是i/2

3、如果希望找到的是最小值,那么从最后一个非叶子节点开始,该节点的值是其左右子节点中值较小者的索引,比如上图中的“ls[4]”处的非叶子节点,他的左子节点的值是6,右子节点的值是12,那么它的值应该是值为6的左子节点的索引8(不要看上图中的值是3,我的实现更为容易理解,和图中所在文章的实现实现的不一样)

4、同理,"ls[3]"处节点的值,应该是值为9的左子节点的索引6

5、"ls[2]"处节点怎么计算?它的左子节点的值由第三步计算得到是8,代表的是"胜者是索引为8值为6的叶子节点",右子节点是叶子节点值为10,那么还是索引为8值为6的节点获胜,所以它的值也是8

6、同理,"ls[1]"处的节点的值,比较的是左子节点索引为8值为6,右子节点索引为6值为9,右子节点获胜,所以它的值应该是8

7、即最终根节点的值为6,代表所有叶子节点中最小值是索引为8的叶子节点,值为6

8、以上就是通过胜者树对[10,9,20,6,12]获取最小值的全过程,一共比较了几次?没有5次吧。胜者树获取极值的时间复杂度为logN


形成胜者树的方法:

1、如果数据集的长度是N,那么构造一个2 * N的数组,和二叉堆一样数组的第一个成员废弃(为了便于使用完全二叉树的特点,上面的第2步说明),即N个叶子节点 + (N - 1)个非叶子节点 + 数组首成员废弃

2、把数据集的所有元素,放在数组的最后

3、从第一个非叶子节点开始,计算它的左右子节点的胜者,记录胜者的索引为该非叶子节点的值

4、直到计算到根节点为止,根节点就会保存的是极值的索引

5、通过由数组获取该索引的值就可以获取到极值了

代码:

wintree.h(类声明):

#include <vector>

template<class T> class wintree {
	T *treenode;
	int nodenum;
	void adjust(int i);
	int lchild(int i){return i * 2;}
	int rchild(int i){return i * 2 + 1;}
public:
	wintree(T *data, int size);
	wintree(std::vector<T> data);
	~wintree(){delete []treenode;}
	T getmax();
	void modifyroot(T value);
};

wintree_func.h(类实现):

#include "wintree.h"
#include <iostream>


template<class T> wintree<T>::wintree (T *data, int size) {
	treenode = new T[size * 2];
	for (int i = 0; i < size; i++) {
		treenode[size + i] = data[i];
	}
	nodenum = size * 2;

	for (int i = nodenum/2 - 1; i >= 1; i--) {
		//从最后一个非叶子节点开始, 每一局比赛
		adjust(i);
	}
}

template<class T> wintree<T>::wintree (std::vector<T> data) {
	treenode = new T[data.size() * 2];
	for (int i = 0; i < data.size(); i++) {
		treenode[data.size() + i] = data[i];
	}
	nodenum = data.size() * 2;

	for (int i = nodenum/2 - 1; i >= 1; i--) {
		//从最后一个非叶子节点开始, 每一局比赛
		adjust(i);
	}
}

//lchild_indexh和rchild_index为其真实左右子节点的索引, leaveindex_l和leaveindex_r是其真实应该保存的胜者的索引
//如果lchild_index或rchild_index已经不是叶子节点, 那么它的胜者应该从它的左/右子节点保存的索引获取
template<class T> void wintree<T>::adjust (int i) {
	int lchild_index = lchild(i), rchild_index = rchild(i);
	int leaveindex_l, leaveindex_r;
	if (lchild_index < nodenum/2) {
		leaveindex_l = treenode[lchild_index];
	} else {
		leaveindex_l = lchild_index;
	}

	if (rchild_index < nodenum/2) {
		leaveindex_r = treenode[rchild_index];
	} else {
		leaveindex_r = rchild_index;
	}

	treenode[i] = (treenode[leaveindex_l] > treenode[leaveindex_r])?leaveindex_r:leaveindex_l;
}

template<class T> T wintree<T>::getmax () {
	return treenode[treenode[1]];
}

template<class T> void wintree<T>::modifyroot (T value) {
	treenode[treenode[1]] = value;
	for (int i = treenode[1]/2; i >= 1; i = i/2) {
		adjust(i);
	}
}


wintree.cpp(测试程序):

#include "wintree_func.h"

int main () {
	int testdata[] = {3,1,2,4,5,12,0,-1};
	wintree<int> wtree(testdata, 8);
	std::cout << wtree.getmax() << std::endl;
	wtree.modifyroot(20);
	std::cout << wtree.getmax() << std::endl;

	return 0;
}

相比于典型的胜者树实现,本实现应该更为容易理解一些。

如果在胜者树构建获取极值后,我修改了原始数据集,那么在原始胜者树中可以非常快速的重新获取极值,方法就是从极值索引的父节点开始,按层向上重新获取极值,就是代码中的modifyroot方法,理解了二叉树就非常好理解。

这个特点也是胜者树败者树利于多路归并排序的一个原因。


2、败者树

败者树是在胜者树基础上的一些小修改:

1、败者树的每个非叶子节点保存的不再是胜者的索引,而是败者的索引

2、但是在它自己作为子节点参赛时,却以当时的胜者索引去参赛

3、最终的根节点保存的虽然是败者的索引,但是根节点上面一层还要有一个更根的节点(对于数组的首成员,胜者树废弃),保存最终胜者的索引

如下图:


获取最小值,注释:

1、“ls[4]”处保存的是叶子节点b3和b4的败者即b4值为12的右叶子节点

2、但在它参数时,即处理“ls[2]”时,“其左子节点ls[4]”,以刚才的胜者b3值为6的节点出战

3、直到根节点"ls[1]",依然保存败者索引b1

4、但根节点更上层的节点"ls[0]"保存最终的胜者索引即b3值为6的叶子节点索引

代码:

losetree.h(类声明):

#include <vector>

template<class T> class losetree {
	int *treenode;
	int *winnode;
	int nodenum;
	void adjust(int i);
	int lchild(int i){return i * 2;}
	int rchild(int i){return i * 2 + 1;}
public:
	losetree(T *data, int size);
	losetree(std::vector<T> data);
	~losetree(){delete []treenode; delete []winnode;}
	T getmax();
	void modifyroot(T data);
};
losetree_func.h(类实现):

#include "losetree.h"
#include <iostream>


template<class T> losetree<T>::losetree (T *data, int size) {
	treenode = new int[size * 2];
	winnode = new int[size - 1];
	for (int i = 0; i < size; i++) {
		treenode[size + i] = data[i];
	}
	nodenum = size * 2;

	for (int i = nodenum/2 - 1; i >= 1; i--) {
		adjust(i);
	}
	treenode[0] = winnode[1];
}

template<class T> losetree<T>::losetree (std::vector<T> data) {
	treenode = new int[data.size() * 2];
	winnode = new int[data.size() - 1];
	for (int i = 0; i < data.size(); i++) {
		treenode[data.size() + i] = data[i];
	}

	nodenum = data.size() * 2;

	for (int i = nodenum/2 - 1; i >= 1; i--) {
		adjust(i);
	}
	treenode[0] = winnode[1];
}

template<class T> void losetree<T>::adjust (int i) {
	int lchild_index = lchild(i), rchild_index = rchild(i);
	int leaveindex_l, leaveindex_r;
	if (lchild_index < nodenum/2) {
		leaveindex_l = winnode[lchild_index];
	} else {
		leaveindex_l = 2 * i;
	}

	if (rchild_index < nodenum/2) {
		leaveindex_r = winnode[rchild_index];
	} else {
		leaveindex_r = 2 * i + 1;
	}

	treenode[i] = (treenode[leaveindex_l] > treenode[leaveindex_r])?leaveindex_l:leaveindex_r;
	winnode[i] = (treenode[leaveindex_l] > treenode[leaveindex_r])?leaveindex_r:leaveindex_l;
}

template<class T> T losetree<T>::getmax () {
	return treenode[treenode[0]];
}

template<class T> void losetree<T>::modifyroot (T data) {
	treenode[treenode[0]] = data;
	for (int i = treenode[0]/2; i >= 1; i--) {
		adjust(i);
	}
	treenode[0] = winnode[1];
}

losetree.cpp(测试程序):

#include "losetree_func.h"

int main () {
	int testdata[] = {3,1,2,5,4};
	losetree<int> ltree(testdata, 5);
	std::cout << ltree.getmax() << std::endl;
	ltree.modifyroot(100);
	std::cout << ltree.getmax() << std::endl;
	return 0;
}

败者树在实现上使用了一个辅助的数组,用于记录每个非叶子节点的实际参赛胜者,其他和胜者树相同。

败者树的重构成,即修改了极值点的数值后再次获取极值,思路原理和胜者树完全一样。

胜者树、败者树获取数据集极值的时间复杂度都是logN。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值