上一篇文章写了普通归并排序即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。