大部分人称呼它们为“胜者树”和“败者树”,也有人称呼它们为“优胜树”和“淘汰树”,我觉得还是优胜树和淘汰树比较好听点。
优胜树是完全二义树,每个结点的取值足两个孩子的较小值。根据定义,根结点的取值是整个树的最小值。
这里给出了八路大军的前三排。
如果规定关键字较小的记录获胜,则优胜树与锦标赛的晋级过程相似,每个非叶结点都对应一场比赛的获胜选手,根是赛事的胜者,其关键字最小。由于每个结点通常占用的存储空间较大,为节省空间,在优胜树的归并过程,可用指针指向每路序列的第一个记录,图中的根结点仅包含一个指针,指向第4路的第一个记录。
为什么要这样呢?或者说,看到这里,依然是觉得这棵树平平无奇的。
不急,我们来看看优胜树的重构: 以上面的例子为例,取出了第一个“6”之后,第四排及时的补上了一个“15”, “15”和旁边的“20”进行比较,选出来“15”, “15”再和旁边的“9”进行比较,选出了“9”, “9”再和旁边的“8”进行比较,选出了第二个值:8。
从这个演示可以看出这个算法真正吸引我们的地方就是当决出一个胜者后,要取得下一个胜者的比较只限于从根到刚才选出的外结点这一条路径上。可以看出除第一次比较需要n-1次外,此后选出次小,再次小…的比较都是log n次,故其复杂度为O(nlogn)。但是对于有n个待排元素,锦标赛算法需要至少2n-1个结点来存放胜者树。故这是一个拿空间换时间的算法。
#include<iostream>
#include<vector>
using namespace std;
class Tree_Node {
private:
success_tree* father; //每个节点都会有父节点
int val; //当前节点的值
public:
Tree_Node(int val) :val(val) {}
};
vector<int> return_temp(vector<int>& temp) {
vector<int> t;
for (int i = 0, j = 1; j< temp.size(); i += 2, j += 2) {
t.push_back(max(temp[i], temp[j]));
}
return t;
}
class success_tree {
public:
vector<vector<int>> res;
vector<vector<int>> target;
success_tree(vector<vector<int>>& target) {
this->target = target;
}
vector<vector<int>> create_tree() {
/*
参数:待排序的归并序列
操作方法:
1、遍历当前归并序列,取出每个序列的尾部数据,设置序列数为2的n次方
2、获得第一批父节点,存入一组数组中
3、再获取一批父节点,存入下一组数组中
4、重复步骤三,直到某组数组中只有一个数据
返回值:
该二维数组
*/
int sz = target.size();
vector<int> temp;
for (int i = 0; i < sz;i++) {
temp.push_back(target[i].back());
}
temp = return_temp(temp);
do{
temp = return_temp(temp);
res.push_back(temp);
} while (temp.size() > 1);
}
void run() {
/*
函数功能:完成排序
操作手法:
1、取出根节点之后,跟尾结点进行比对,看是哪一个分支分出的
2、尾结点向前递进,采用减一除二的推导式
3、如果有某个分支为空,则设置该位置为INT_MAX,让它们永远没有机会再出现
4、记录一个标志位,标记全部为空的时候退出循环
*/
int flag = target.size(); //4
while (flag) {
int out = target.back()[0];
cout << out << endl; //这里就不弄其他的了,直接输出吧
for (int i = 0; i < res.size(); i++) {
for (int j = 0; j < res[i].size(); j++) {
if (out == res[i][j]) { //1
if (i == 0) {
target[i].pop_back();
if (target[i].empty()) { //3
target[i].push_back(INT_MAX);
flag--;
}
res[i][j] = target[i].back();
}
else {
res[i][j] = max(res[i-1][2*j], res[i - 1][2 * j + 1]);
}
}
}
}
}
}
};
对于胜者树来说,在节点上升的时候首先需要获得父节点,然后再获得兄弟节点,然后再比较。这时人们又想能否减少访存次数,于是就有了败者树。
在败者树中,用父结点记录其左右子结点进行比赛的败者,让胜者参加下一轮的比赛。败者树的根结点记录的是败者,因此,需要加一个结点来记录比赛的最终胜者。
败者树重构过程如下:①首先,将新进入败者树的新结点与其父结点进行比较,并将败者存放在父结点中;②然后,将第1步中比较后的胜者再与上一级的父结点比较。
因为重构时覆盖原来叶子节点的下一个节点都比原叶子节点小,这里的小是指胜利的反方向,所以只要和败者比即可。此时父节点记录的就是败者,因此,只要和父节点比较即可。所以说对于败者树来说,它只要访问父节点,这是败者树的优势。
晕不?我也晕呐,看了半天我才缓过来,值小的为胜者,值大的为败者。。。。。 把这个观念扭过来,然后我们再看。
这是一张比较经典的图,大家都在用:
a:b3 Vs b4,b3胜b4负,内部结点ls[4]的值为4,表示b4为败者;胜者b3继续参与竞争。
b:b3 Vsb0,b3胜b0负,内部结点ls[2]的值为0,表示b0为败者;胜者b3继续参与竞争。
c:b1 Vs b2,b1胜b2负,内部结点ls[3]的值为2,表示b2为败者;胜者b1继续参与竞争。
d:b3 Vs b1,b3胜b1负,内部结点ls[1]的值为1,表示b1为败者;胜者b3为最终冠军,用ls[0]=3,记录的最后的胜者索引。
捋一下?捋清楚思路我们再往下,搞红黑树的时候都没这么折磨人,小者为胜,这个思想要扭过来。
void Adjust(int s)
{
int t=(s+k)/2; //t为内部结点,也就是s的父节点
while(t>0){
if(b[s]>b[ls[t]]){
int tmp=s;
s=ls[t]; //s记录新的胜者
ls[t]=tmp;
}
t=t/2;
}
ls[0]=s;
}
void CreateLoserTree()
{
int i;
b[k]=MIN;
for(i=0;i<k;i++)
ls[i]=k;
for(i=k-1;i>=0;i--)
Adjust(i);
}
难搞哦。。