二叉树
1. 二叉树的遍历
1.1 递归DFS遍历
二叉树的遍历过程分为自顶向下和自底向上,两种递归处理方式。
树的左右两条边,像两个通道一样,父节点可以通过函数参数,向子节点传递数据;子节点执行完后,通过返回值向父节点进行反馈。
自顶向下,节点在得到上一节点传递的值,进行处理后,确定向下传递的值。
自底向上,节点得到左右节点返回值,从而确定自己的返回值。
在编写递归程序时,需要先确定基准情况,基准情况就是不需要处理直接返回的情况。
一般可以理解为:遍历到叶节点,就返回自己的值;遍历到值>x,就返回false等情形。
自顶向下
自顶向下隐含意思是,当前节点得到父节点传过来的信息,对当前节点进行判断,并得到向下传递的数据。
自顶向下的一般框架:
void fun(root,x)
{
//基准情况
//根据x,与当前节点值进行处理.
do something with x and root->val;
//根据父节点传递的x和root->val,确定向下传递值y
fun(root->left,y);
fun(root->right,y);
}
自底向上
自底向上递归的含义是,节点先接收到左右节点的返回值,进行处理,并确定向上传递的值。
自底向上一般框架:
int fun(root)
{
//基准情况
//得到左右值
int left=fun(root->left);
int right=fun(root->right);
//在当前节点进行处理,并得到向上传递值res.
return res;
}
向下传递且自底向上
树通过函数参数列表,向下传递信息。
通过返回值或(参数列表中引用)向上传递信息。
int fun(root,x)
{
1.处理基准情况
2.根据父节点x和root->val,确定向下传递值Z
int left=fun(root->left,Z);
int right=fun(root->right,Z);
3.得到左右节点返回值A,B,确定当前节点返回值D
return D;
}
因此,在树的递归程序中,有两处可以处理当前节点:
拿到父节点值时,处理当前节点,并确定向下传递参数
拿到左右节点值时,处理当前节点,并确定返回值。
1.2 实例
1.2.1 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
自底向上解法
首先考虑,当前节点在拿到左右子树深度的情况下,就能得到当前子树深度max(left,right)+1。
基准条件就是NULL节点,NULL节点向上传递值为0,且当前节点不需要向下传递参数。
这一题,树先从根部向下进行递归遍历,当碰到基准条件时返回。
以节点7为例,当向下遍历到NULL时,返回0;
节点7得到左右节点(NULL)返回的0,对当前节点进行处理,得到向上传递的返回值max(0,0)+1
节点20在得到其左右两节点的返回值后,进行处理,得到向上传递值。
最终得到深度值为3,把树两遍的连接通道看做是,函数的返回值,子节点会向上进行传递。
自顶向下解法
自顶向下可以理解为当节点接收到父节点传递的深度信息,可以知道当前节点的深度。
基准条件是遇到NULL节点,返回深度值。
在编写程序前,先把向下传递和向上传递的信息流图画出。
从这个信息流动图,可以看到节点向下传递当前深度值,向上传递左右节点返回值中的最大值。
同时确定了基准条件,即遇到NULL节点返回当前深度值。
因此,可以得到程序
int int maxDepth(struct TreeNode* root,int deep){
//基准条件
if(root==NULL)
return deep;
//根据deep得到向下传递值.
//得到左右节点的返回值.
int left=maxDepth(root->left,deep+1);
int right=maxDepth(root->right,deep+1);
//根据左右节点返回值 得到当前节点返回值
return max(left,right);
}
1.2.2 路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22
,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true
, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2
。
首先,此问题要求解路径和是否为sum,树有n个叶节点就有n条路径,因此应该在叶节点处判断是否满足sum和。
因此,此问题应该使用自顶向下的解法,将一个问题抛给下面的叶节点处进行处理,并将处理的n个结果整理成一个结果。
(改正)这个程序应该包含自顶向下,同时也存在自底向上,即在叶节点也处理了,在拿到左右节点返回值后也进行处理了。
不过自顶向下和自底向上处理的逻辑不同,
自顶向下将问题发散到n个叶节点,每一个都会判断所通过的路径和是否为sum;
自底向上,将这些叶节点的数据向上传递,通过一个逻辑,汇总成true或false。
此处,主要的处理逻辑是判断路径和,因此可以看做是自顶向下处理,即将问题发散了,让每一个叶节点去处理。
画出树的信息传递图:
从图上可以看到,节点向下传递当前路径和,向上传递是否满足路径和为sum的bool信息。
因此,构思出程序细节:
-
基准条件
基准条件是在遍历到叶节点时返回,同时需要确定叶节点的返回值,应该是sum==n+root->val;
-
向下传递值
节点会接收到父节点传递的路径和n,向下传递n+root->val;
-
根据左右值得到返回值
在得到左右节点返回值后,确定自己的返回值left||right。
程序实现:
/*向下传递 sum:路径和判断值 n:当前路径和*/
bool helper(struct TreeNode* root, int sum,int n)
{
//基准条件,在叶节点做判断 路径和是否为sum
if(!root->left&&!root->right)
return sum==n+root->val;
//确定向下传递值.
n=root->val+n;
//得到左右子树信息
bool left,right;
left=right=false;
if(root->left)
left=helper(root->left,sum,n);
if(root->right)
right=helper(root->right,sum,n);
//根据左右子树信息 确定向上返回值,
return left||right;
}
1.2.3 验证二叉搜索树 (98)
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
判断一个树root是二叉搜索树,需要此节点左侧所有节点均小于root,其右儿子均大于root,同时左右子树也为二叉搜索树。
因此,若采用自底向上的思路,需要在此节点处判断 2个信息
- 左侧所有节点小于此节点
- 右侧所有节点大于此节点
想要判断这两个信息,需要其子节点向上传递所有节点信息。
若采用自顶向下的思路,可以理解为,根据节点将其左右子树的节点范围限定为(-∞,root)和(root,+∞)
后序节点在接到父节点传递的信息后,可以得到向下传递的信息;
基准条件就是 不需要继续向下遍历就能知道返回值的情形。
在此处应该是NULL节点,遍历到空节点时,返回true;同时若当前节点不满足节点范围,返回false;
在有了基本思路后,画出树的信息传递图:
有了信息传递图,我们可以确定程序基本框架:
-
基准条件
基准条件隐含的意思是,不需要向下遍历,就知道返回值;
在此处应有两个,遍历到NULL时,返回true;当前节点不在范围内,返回false;
-
根据父节点传递值得到向下传递值
根据父节点传递的范围(min,max),再根据当前节点值,确定向左子树传递(min,root),向右子树传递(root,max)
-
根据左右节点回馈,得到此节点回馈
根据左右节点的反馈值,若左右子树均为二叉树,则此树为二叉树;(对当前节点的判断,已经放在了基准条件中)。
程序实现:
bool helper(struct TreeNode* root,long min,long max);
bool isValidBST(struct TreeNode* root){
return helper(root,LONG_MIN,LONG_MAX);
}
bool helper(struct TreeNode* root,long min,long max)
{
//基准条件
if(root==NULL)
return true;
if(root->val >= max || root->val <=min)
return false;
//计算得到向下传递参数.
//得到左右子树返回信息.
bool left=helper(root->left,min,root->val);
bool right=helper(root->right,root->val,max);
//计算得到返回值.
return left&&right;
}
探索
其实此程序还有另一种写法,先贴上代码:
bool helper(struct TreeNode* root,long min,long max);
bool isValidBST(struct TreeNode* root){
return helper(root,LONG_MIN,LONG_MAX);
}
bool helper(struct TreeNode* root,long min,long max)
{
//基准条件
if(root==NULL)
return true;
//得到左右子树返回信息.
bool left=helper(root->left,min,root->val);
bool right=helper(root->right,root->val,max);
//计算得到返回值.
return left&&right&&root->val>min&&root->val<max;
}
这个程序与上面一种写法的不同之处在于,其不将中间节点(能判断返回值)作为基准条件,保持统一,仅在获得了左右节点返回值后,对当前节点进行处理。当然,这种写法会导致不必要的向下遍历,因为在已知当前节点不满足条件时,已经没必要再知道左右子树的返回值就已经能确定当前节点返回值了。
1.3 层次遍历框架
二叉树的层次遍历需要借助队列进行;
void fun(root)
{
queue<TreeNode* > queue;
queue.push(root);
TreeNode* last=root;
while(!queue.empty())
{
//出列
//左右儿子入列
//一层处理完,更新last
}
}
1.4 二叉树先后顺序遍历
自顶向下和自底向上,本质上就是树的前序遍历和后序遍历,还有一种是树的中序遍历,即按左中右的顺序去处理。
基本框架
//基准条件
//得到左节点返回值
//根据左节点返回值,处理当前节点,并确定向右节点传输值.
//得到右节点返回值,确定当前节点返回值.
实例
二叉搜索树第k大节点(面试题54)
给定一棵二叉搜索树,请找出其中第k大的节点。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 4
这一题就是典型的中序遍历的应用,需要按顺序进行遍历。
先画出信息流图
中序遍历可以将递归调用步骤分为5步,
- 先根据父节点值,确定向左节点传递值,并调用左节点
- 得到左节点数据,并根据左节点数据进行处理
- 处理后,确定向右节点传递值,并调用右节点
- 得到右节点返回值
- 根据左右节点返回值和当前节点值,处理得到向上传递值
当前节点有两个处理程序,为步骤2和步骤5.
其中步骤2为主要处理,而步骤5仅仅是确定向上返回值。
根据此思路,写程序
int kthLargest(TreeNode* root, int k) {
int x;
helper(root,k,0,x);
return x;
}
//k是固定参数,第K个值
//n是向下传递的值
//x是每个节点都可以访问的存储变量
int helper(TreeNode* root,int k,int n,int &x)
{
//基准条件
if(root==NULL)
return n;
if(n==k)
return n;
//根据上层信息,确定向右侧传递信息
//得到右侧信息(右侧处理了right个节点)
int right=helper(root->right,k,n,x);
//根据右侧信息处理当前节点
if(k==right+1)
x=root->val;
//向左侧传递信息,并得到左侧数据
int left=helper(root->left,k,right+1,x);
//根据数据 确定返回值.
return left;
}
1.5 二叉树节点的位置表示法
二叉树在树中当前层的位置可以表示为i
,那么就有其左二子位置为2*i
,右儿子位置为2*i+1