二叉树是一种特殊的树,每次分叉不超过两部分。二叉树作为数据结构是非常重要和基础的。无论是二叉堆、线段树还是平衡树,这些高级的数据结构都以二叉树为基础。多叉树可以转换成二叉树,而更复杂的一些树结构会在后面高级中介绍。
我们举个例子。
【题目问题】
淘汰赛(1)。淘汰赛有(n7)个国家参加世界杯决赛圈而进入淘汰赛环节。已经知道各个国家的能力值,且都不相等。能力值高的国家和能力值低的国家踢比赛时高者获胜。1号国家和2号国家踢一场比赛,胜者晋级。3号国家和4号国家也踢一场,胜者晋级......晋级后的国家继续用相同的方式继续完成赛程,知道决出冠军。给出各个国家的能力值,请问:亚军是那个国家?
【题目分析】
假设n=3,有8个国家参加,各个国家的能力值分别是(4,2,3,1,10,5,9,7)。可以很容易画出如下图所示的赛程图。
从P1看出,最厉害的5号国家是当之无愧的冠军,但是1号国家实力不怎么样,只是在决赛之前碰到更弱的对手,所以侥幸闯进了决赛;而7号国家也挺厉害,但很不幸在半决赛碰到了5号国家所以惨遭淘汰。可见,并不是第二强的国家一定能拿到亚军。
从P1还可以看到,从冠军(根节点)往下面看,每个获胜者节点下面都有2个国家。这就是一棵典型的满二叉树。更加严格的递归定义是:二叉树要么为空,要么为根节点、左子树、右子树构成,二左右子树分别还是一个二叉树(读者可以验证一下这个赛程图是否符合上面的定义)。如果一个结点没有任何子树,那就称为叶子节点。
这个赛程图经过了3轮比赛,一共有4层节点,所以这个二叉树的高度是4.如果一个二叉树的高度为h,从第二层开始每一层的结点数都是上一层的两倍,一共有(-1)个结点的二叉树称为完美二叉树。
对于完美二叉树,可以从上到下,从左到右,对各个结点从1开始分配序号,如下P2所示。
图P1 赛程图↑
图P2 完美二叉树的结点编号↑
有没有发现一些规律?对于i号非叶子结点,它的左子树编号为2*i,右子树的编号是2*i+1.这样就可以创建若干足够大的数组将各个结点的信息记录进去,通过计算编号来访问左右子树,并使用递归的方式得到各个子树的统计,代码如下(代码结构如下,对错未调试,读者自行调试):
【输入样例】
8
4 2 3 1 10 5 9 7
【输出样例】
1
【解题代码】
#include<cstdio>
#include<iostream>
using namespace std;
int value[260],winner[260];
int n;
void dfs(int x){
if(x>=1<<n){
return;
}
else{
dfs(2*x);
dfs(2*x+1);
int lvalue=value[2*x],rvalue=value[2*x+1];
if(lvalue>rvalue){ //左结点获胜
value[x]=lvalue;//记录下获胜方的能力值
winner[x]=winner[2*x];;//和获胜方的编号
}else{//右结点获胜
value[x]=rvalue;
winner[x]=winner[2*x+1];
}
}
}
int main(){
cin>>n;
for(int i=0;i<1<<n;i++){
cin>>value[i+(1<<n)];//读入各个结点的能力值
winner[i+(1<<n)]=i+1;//叶子结点的获胜方就是自己国家的编号
}
dfs(1);//从根节点开始遍历
cout<<value[2]>value[3]?winner[3]:winner[2]; //找出亚军输出
return 0;
}
考虑到这是一个完美二叉树,1到(1<<n)-1编号都是非叶子结点,编号不小于1>>n都是叶子结点。本文使用value[i]来记录叶子结点的实力值,或者是非叶子结点的该子树的最大值;是由winner[i]来记录该子树的获胜者。当所有比赛模式完毕,winner[1]记录了整场比赛的冠军,value[i]记录冠军的实力值。这是要比较一下谁是决赛败者,输出败者的国家编号,即整场比赛的亚军。
由于需要维护和子树相关的两个值——value和winner,所以建立了这两个数字存储字数信息。有时只需要维护子树的一个信息,也可不用建立这样的数组,而是将dfs函数定义为具有返回值函数,然后在递归函数中处理这个值。事实上函数也可以返回两个(甚至是多个)返回值,可以使用STL中的pair容器做到,这里不再详细阐述。
下一章继续讲解,这一章就此结束。