一、二叉树中的深搜
深度优先遍历(DFS,全称为 Depth First Traversal),是我们树或者图这样的数据结构中常用的一种遍历算法。这个算法会尽可能深的搜索树或者图的分支,直到一条路径上的所有节点都被遍历完毕,然后再回溯到上一层,继续找一条路遍历。
在二叉树中,常见的深度优先遍历为:前序遍历、中序遍历以及后序遍历 因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。并且前中后序三种遍历的唯一区别就是访问根节点的时机不同,在做题的时候,选择一个适当的遍历顺序,对于算法的理解是非常有帮助的。
二、示例题目
1.计算布尔二叉树的值. - 力扣(LeetCode)
给你一棵 完整二叉树 的根,这棵树有以下特征:
- 叶子节点 要么值为
0
要么值为1
,其中0
表示False
,1
表示True
。 - 非叶子节点 要么值为
2
要么值为3
,其中2
表示逻辑或OR
,3
表示逻辑与AND
。
计算 一个节点的值方式如下:
- 如果节点是个叶子节点,那么节点的 值 为它本身,即
True
或者False
。 - 否则,计算 两个孩子的节点值,然后将该节点的运算符对两个孩子值进行 运算 。
返回根节点root
的布尔运算值。
完整二叉树 是每个节点有 0
个或者 2
个孩子的二叉树。
叶子节点 是没有孩子的节点。
示例 1:
输入:root = [2,1,3,null,null,0,1]
输出:true
解释:上图展示了计算过程。
AND 与运算节点的值为 False AND True = False 。
OR 运算节点的值为 True OR False = True 。
根节点的值为 True ,所以我们返回 true 。
示例 2:
输入:root = [0]
输出:false
解释:根节点是叶子节点,且值为 false,所以我们返回 false 。
解法(递归)
算法思路:
本题可以被解释为:
1.对于规模为 n的问题,需要求得当前节点值。
2.节点值不为0或1时,规模为n的问题可以被拆分为规模为 n-1的子问题:
a.所有子节点的值;
b.通过子节点的值运算出当前节点值。
3.当问题的规模变为 n=1时,即叶子节点的值为0或1,我们可以直接获取当前节点值为0或 1。
算法流程:
递归函数设计:boolevaluateTree(TreeNode*root)
1.返回值:当前节点值;
2.参数:当前节点指针。'
3.函数作用:求得当前节点通过逻辑运算符得出的值。
递归函数流程:
1.当前问题规模为 n=1时,即叶子节点,直接返回当前节点值;1.
2.递归求得左右子节点的值;
- 通过判断当前节点的逻辑运算符,计算左右子节点值运算得出的结果
cpp
class Solution {
public:
bool evaluateTree(TreeNode* root)
{
if(root->left==nullptr)
return root->val;
bool left=evaluateTree(root->left);
bool right=evaluateTree(root->right);
return root->val==2?left|right:left&right;
}
};
2.求根节点到叶子结点数字之和. - 力扣(LeetCode)
给你一个二叉树的根节点 root
,树中每个节点都存放有一个 0
到 9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3
表示数字123
。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2
代表数字 12
从根到叶子节点路径 1->3
代表数字 13
因此,数字总和 = 12 + 13 = 25
示例 2:
输入:root = [4,9,0,5,1]
输出:1026
解释:
从根到叶子节点路径 4->9->5
代表数字 495
从根到叶子节点路径 4->9->1
代表数字 491
从根到叶子节点路径 4->0
代表数字 40
因此,数字总和 = 495 + 491 + 40 = 1026
解法(dfs-前序遍历)
前序遍历按照根节点、左子树、右子树的顺序遍历二叉树的所有节点,通常用于子节点的状态依赖于父节点状态的题目 。
算法思路:
在前序遍历的过程中,我们可以往左右子树传递信息,并且在回溯时得到左右子树的返回值。递归函数可以帮我们完成两件事:
1.将父节点的数字与当前节点的信息整合到一起,计算出当前节点的数字,然后传递到下一层进行递归;
2.当遇到叶子节点的时候,就不再向下传递信息,而是将整合的结果向上一直回溯到根节点。
在递归结束时,根节点需要返回的值也就被更新为了整棵树的数字和。
算法流程:
递归函数设计:int dfs(TreeNode*root,int num)
1.返回值:当前子树计算的结果(数字和);
2.参数 num:递归过程中往下传递的信息(父节点的数字);
3.函数作用:整合父节点的信息与当前节点的信息计算当前节点数字,并向下传递,在回溯时返回当
前子树(当前节点作为子树根节点 )数字和。
递归函数流程:
1.当遇到空节点的时候,说明这条路从根节点开始没有分支,返回0;
2.结合父节点传下的信息以及当前节点的 val,计算出当前节点数字 sum;
3.如果当前结点是叶子节点,直接返回整合后的结果 sum;
4.如果当前结点不是叶子节点,将 sum 传到左右子树中去,得到左右子树中节点路径的数字和,然后相加后返回结果。
cpp
class Solution {
public:
int sumNumbers(TreeNode* root)
{
int ret=dfs(root,0);
return ret;
}
int dfs(TreeNode*root,int sum)
{
sum=sum*10+root->val;
if(root->left==nullptr&&root->right==nullptr)
return sum;
int ret=0;
if(root->left)
ret+=dfs(root->left,sum);
if(root->right)
ret+=dfs(root->right,sum);
return ret;
}
};
3.二叉树剪枝. - 力扣(LeetCode)
给你二叉树的根结点 root
,此外树的每个结点的值要么是 0
,要么是 1
。
返回移除了所有不包含 1
的子树的原二叉树。
节点 node
的子树为 node
本身加上所有 node
的后代。
示例 1:
输入:root = [1,null,0,0,1]
输出:[1,null,0,null,1]
解释:
只有红色节点满足条件“所有不包含 1 的子树”。 右图为返回的答案。
示例 2:
输入:root = [1,0,1,0,0,0,1]
输出:[1,null,1,null,1]
示例 3:
输入:root = [1,1,0,1,1,0,1,0]
输出:[1,1,0,1,1,null,1]
解法(dfs-后序遍历)
后序遍历按照左子树、右子树、根节点的顺序遍历二叉树的所有节点,通常用于父节点的状态依赖于子节点状态的题目。
算法思路:
如果我们选择从上往下删除,我们需要收集左右子树的信息,这可能导致代码编写相对困难。然而,通过观察我们可以发现,如果我们先删除最底部的叶子节点,然后再处理删除后的节点,最终的结果并不会受到影响。
因此,我们可以采用后序遍历的方式来解决这个问题。在后序遍历中,我们先处理左子树,然后处理右子树,最后再处理当前节点。在处理当前节点时,我们可以判断其是否为叶子节点且其值是否为0如果满足条件,我们可以删除当前节点。
需要注意的是,在删除叶子节点时,其父节点很可能会成为新的叶子节点。因此,在处理完子节点后,我们仍然需要处理当前节点。这也是为什么选择后序遍历的原因(后序遍历首先遍历到的一定是叶子节点)。
通过使用后序遍历,我们可以逐步删除叶子节点,并且保证删除后的节点仍然满足删除操作的要求。这样,我们可以较为方便地实现删除操作,而不会影响最终的结果。
若在处理结束后所有叶子节点的值均为 1,则所有子树均包含 1,此时可以返回。
算法流程:
递归函数设计:void dfs(TreeNode*&root)
1.返回值:无;
2.参数:当前需要处理的节点;
3.函数作用:判断当前节点是否需要删除,若需要删除,则删除当前节点。
后序遍历的主要流程:
1.递归出口:当传入节点为空时,不做任何处理;
2.递归处理左子树;
3.递归处理右子树;
4.处理当前节点:判断该节点是否为叶子节点(即左右子节点均被删除,当前节点成为叶子节点)并且节点的值为 0。
a.如果是,就删除掉;
b.如果不是,就不做任何处理。
cpp
class Solution {
public:
TreeNode* pruneTree(TreeNode* root)
{
if(root==nullptr)
return nullptr;
root->left=pruneTree(root->left);
root->right=pruneTree(root->right);
if(root->left==nullptr&&root->right==nullptr&&root->val==0)
{
delete root;
root=nullptr;
}
return root;
}
};
4.验证二叉搜索树. - 力扣(LeetCode)
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
-
节点的左 子树
只包含小于当前节点的数。
-
节点的右子树只包含 大于 当前节点的数。
-
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
解法(利用中序遍历)
中序遍历按照左子树、根节点、右子树的顺序遍历二叉树的所有节点,通常用于二叉搜索树相关题目。(中序遍历的结果是一个有序的序列)
算法思路:
如果一棵树是二叉搜索树,那么它的中序遍历的结果一定是一个严格递增的序列 。因此,我们可以初始化一个无穷小的全区变量,用来记录中序遍历过程中的前驱结点。那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,然后修改前驱结点为当前结点,传入下一层的递归中。
算法流程:
- 初始化一个全局的变量 prev,用来记录中序遍历过程中的前驱结点的 val;
2.中序遍历的递归函数中;
a.设置递归出口: root == nullptr 的时候,返回 true;
b.先递归判断左子树是否是二叉搜索树,用left 标记;
c.然后判断当前结点是否满足二叉搜索树的性质,用cur 标记:
如果当前结点的 val大于 prev,说明满足条件,cur 改为 true;
如果当前结点的 val小于等于 prev,说明不满足条件,retcur 改为 false;
最后递归判断右子树是否是二叉搜索树,用right 标记;
3.只有当 left、 cur 和right 都是 true 的时候,才返回 true。
cpp
class Solution
{
long prev=LONG_MIN;
public:
bool isValidBST(TreeNode* root)
{
if(root==nullptr)
return true;
bool left=isValidBST(root->left);
//剪枝
if(left==false)
return false;
bool cur=false;
if(root->val>prev)
cur=true;
//剪枝
if(cur==false)
return false;
prev=root->val;
bool right=isValidBST(root->right);
return left&&right&&cur;
}
};
5.二叉搜索树中第K小的元素. - 力扣(LeetCode)
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
小的元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
解法(中序遍历+计数器剪枝)
算法思路:
我们可以根据中序遍历的过程,只需扫描前k个结点即可。因此,我们可以创建一个全局的计数器 count,将其初始化为k,每遍历一个节点就将 count--。直到某次递归的时候,count 的值等于1,说明此时的结点就是我们要找的结果。
算法流程:
1.定义一个全局的变量 count,在主函数中初始化为k的值(不用全局也可以,当成参数传入递归过程中);
递归函数的设计:int dfs(TreeNode*root):
返回值为第k个结点;
递归函数流程(中序遍历):
1.递归出口:空节点直接返回-1,说明没有找到;
2.去左子树上查找结果,记为 retleft:
a.如果retleft==-1,说明没找到,继续执行下面逻辑;
b.如果 retleft!=-1,说明找到了,直接返回结果,无需执行下面代码(剪枝);
3.如果左子树没找到,判断当前结点是否符合;
a.如果符合,直接返回结果
4.如果当前结点不符合,去右子树上寻找结果。
cpp
class Solution {
int count;
int ret;
public:
int kthSmallest(TreeNode* root, int k)
{
count=k;
dfs(root);
return ret;
}
void dfs(TreeNode*root)
{
if(root==nullptr||count==0)
return;
dfs(root->left);
count--;
if(count==0)
ret=root->val;
dfs(root->right);
}
};
6.二叉树的所有路径. - 力扣(LeetCode)
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
示例 2:
输入:root = [1]
输出:["1"]
解法(回溯)
算法思路:
使用深度优先遍历(DFS)求解。
路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加入到路径中,如果该节点为叶子节点,将路径存储到结果中。否则,将"->"加入到路径中并递归遍历该节点的左右子树。定义一个结果数组,进行递归。递归具体实现方法如下:
1.如果当前节点不为空,就将当前节点的值加入路径 path 中,否则直接返回;
2.判断当前节点是否为叶子节点,如果是,则将当前路径加入到所有路径的存储数组 paths 中;
3.否则,将当前节点值加上"->"作为路径的分隔符,继续递归遍历当前节点的左右子节点。
4.返回结果数组。
特别地,我们可以只使用一个字符串存储每个状态的字符串,在递归回溯的过程中,需要将路径中的当前节点移除,以回到上一个节点。具体实现方法如下:
1.定义一个结果数组和一个路径数组。
2.从根节点开始递归,递归函数的参数为当前节点、结果数组和路径数组。
a.如果当前节点为空,返回。
b.将当前节点的值加入到路径数组中。
c.如果当前节点为叶子节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果数组中。
d.递归遍历当前结点的左子树。
e.递归遍历当前结点的右子树。
f.回溯,将路径数组中的最后一个元素一处,以返回到上一个结点。
3.返回结果数组。
cpp
class Solution {
vector<string> ret;
public:
vector<string> binaryTreePaths(TreeNode* root)
{
if(root==nullptr)
return ret;
string path;
dfs(root,path);
return ret;
}
void dfs(TreeNode*root,string path)
{
path+=to_string(root->val);
if(root->left==nullptr&&root->right==nullptr)
{
ret.push_back(path);
return;
}
path+="->";
if(root->left)
dfs(root->left,path);
if(root->right)
dfs(root->right,path);
}
};