什么是二叉树
试想一下,我们怎么样才能计算一颗苹果树上有多少颗苹果呢?
其实我们只需要知道左边树权的苹果数量和右边树权的苹果数量,然后计算它们的和就行了。那左边树杈有多少个苹果呢?可以使用一样的方法来统计,把这个分杈当作树干,然后统计这个树干的左边树杈和右边树杈的苹果数量之和……直到统计到树枝末端的每一个苹果,然后依次汇总就可以得到苹果的数量。
显然,树结构不仅能表示数据间的指向关系,还能表示出数据的层次关系,而有很明显的递归性。因此,可以利用树的性质解决更多种类的问题。
二叉树是一种特殊的树,每次分叉不超过两部分。二叉树作为数据结构是非常重要和基础的。无论是二叉堆、线段树还是平衡树,这些高级的数据结构都以二叉树为基础。(下图是一颗树结构)
解释说明:
- 每个节点的左右两个节点分别称为这棵树的左儿子和右儿子。
- 以节点左儿子为根节点的树称为这个节点的左子树,右边同样。
- 如果一个结点没有任何子树,那就称为叶子结点(8、9、10……)
- 这个图一共有4层结点,所以这个二叉树的高度是4。
- 如果一个二叉树的高度为 H H H,从第二层开始每一层的结点数都是上一层的两倍。一共有 2 H − 1 2^H-1 2H−1个结点的二叉树称为完美二叉树。
有没有发现一些规律?对于 i i i 号非叶子结点,它的左子树编号是 2 i 2i 2i 右子树的编号是 2 i + 1 2i + 1 2i+1。由此,我们可以创建若干个足够大的数组,将各个结点的信息记录进去,通过计算编号来访问左右子树,并使用递归的方式得到各个子树的信息。
基础操作
这里比较内容比较冗杂,沉住气一步一步来。
1.从叶子节点反推每一层的状态
问题描述
现有一颗完美二叉树,它每个节点的值都等于他左右儿子的值之和。
现告诉你这棵树有多高,然后从左到右给出叶子节点的权值,请你还原这棵树。
分析
如果我们要用一个数组记录这棵树的节点信息,如下图:
一个有
n
n
n 层的完美二叉树有多少个叶子节点呢?
2
n
−
1
2^{n - 1}
2n−1个。
所以我们可以直接从数组的第
2
n
−
2
n
−
1
2^n-2^{n-1}
2n−2n−1 项开始输入,这样就能直接录入最后一层的信息了。
- 我们可以用位运算表示
2
n
2^n
2n : (1 << n)
现在我们有了录入部分的代码:
for(int i = 0, i < (1 << (n - 1)); i++){
cin >> node[i + (1 << (n - 1))];
}
然后我们可以通过搜索来体现每个节点的值了
void dfs(int x){
if(x >= 1 << (n - 1))return ;
//到达叶子节点之后就开始返回
dfs(x * 2);
//进入左子树
dfs(x * 2 + 1);
//进入右子树
node[x] = node[x*2] + node[x * 2 + 1];
//整合每个节点
}
演示程序:
int node[1000];
int n;
void dfs(int x){
if(x >= 1 << (n - 1))return ;
dfs(x * 2);
dfs(x * 2 + 1);
node[x] = node[x*2] + node[x * 2 + 1];
}
int main(){
cin >> n;
for(int i = 0; i < (1 << (n - 1)); i++){
cin >> node[i + (1 << (n -1))];
}
dfs(1);
int cnt = 1;
for(int i = 1; i <= n; i++){
int t = 1 << (i - 1);
while(t--){
cout << node[cnt++] <<" ";
}
cout << endl;
}
return 0;
}
2.建立一棵二叉树
如果我们要建立一棵树,就得先考虑每个节点需要有什么,最基本的就是能表现出左右儿子在哪,节点权值是多少
所以我们用这样一个结构体来记录节点,-1表示没有左/右儿子:
struct node{
int val, left, right;
node(){
val = 0, left = -1, right = -1;
}
};
node t[N];
每一次我们输入一个节点的权值, 左儿子位置, 右儿子位置,这样就能建立一棵树了
void insert(int loc){
cin >> t[loc].val >> t[loc].left >> t[loc].right;
}
3.一棵树的深度
接上文,我们可以通过DFS遍历一棵树来获得一棵树的深度
int T_deep = 0;
void dfs(int root, int deep){
T_deep = max(T_deep, deep);
if(t[root].left != -1){
dfs(t[root].left, deep + 1);
}
if(t[root].right != -1){
dfs(t[root].right, deep + 1);
}
}
4.遍历一棵树
先序遍历
如果我们按照“中–左–右”的顺序遍历一棵树,这就是先序遍历。
void preOrder(int root){
cout << t[root].val << " ";
if(t[root].left != -1)preOrder(t[root].left);
if(t[root].right != -1)preOrder(t[root].right);
}
同理,我们调换三行的顺序,就可以实现中序遍历和后续遍历。
这些遍历的原理和深度优先遍历是一样的
层次遍历
void bfs(int root){
queue<node>q;
q.push(t[root]);
while(!q.empty()){
node tmp = q.front();
q.pop();
cout << tmp.val <<" ";
if(tmp.left != -1)q.push(t[tmp.left]);
if(tmp.right != -1)q.push(t[tmp.right]);
}
}