0 深度优先遍历
深度优先遍历(DFS,全称为 Depth First Traversal),是我们树或者图这样的数据结构中常⽤的⼀种遍历算法。这个算法会尽可能深的搜索树或者图的分⽀,直到⼀条路径上的所有节点都被遍历完毕,然后再回溯到上⼀层,继续找⼀条路遍历。
在⼆叉树中,常⻅的深度优先遍历为:前序遍历、中序遍历以及后序遍历。
因为树的定义本⾝就是递归定义,因此采⽤递归的⽅法去实现树的三种遍历不仅容易理解⽽且代码很简洁。并且前中后序三种遍历的唯⼀区别就是访问根节点的时机不同,在做题的时候,选择⼀个适当的遍历顺序,对于算法的理解是⾮常有帮助的。
1 计算布尔二叉树的值
1.1 题目链接
1.2 题目描述
给你一棵 完整二叉树 的根,这棵树有以下特征:
- 叶子节点 要么值为
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, 1000]之间。 0 <= Node.val <= 3- 每个节点的孩子数为
0或2。 - 叶子节点的值为
0或1。 - 非叶子节点的值为
2或3。
1.3 解法(递归):
算法思路:
本题可以被解释为:
- 对于规模为 n 的问题,需要求得当前节点值。
- 节点值不为 0 或 1 时,规模为 n 的问题可以被拆分为规模为 n-1 的⼦问题:
- a. 所有⼦节点的值;
- b. 通过⼦节点的值运算出当前节点值。
- 当问题的规模变为 n=1 时,即叶⼦节点的值为 0 或 1,我们可以直接获取当前节点值为 0 或 1。
算法流程:
递归函数设计:bool evaluateTree(TreeNode* root)
- 返回值:当前节点值;
- 参数:当前节点指针。
- 函数作⽤:求得当前节点通过逻辑运算符得出的值。
递归函数流程:
- 当前问题规模为 n=1 时,即叶⼦节点,直接返回当前节点值;
- 递归求得左右⼦节点的值;
- 通过判断当前节点的逻辑运算符,计算左右⼦节点值运算得出的结果;
1.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool evaluateTree(TreeNode* root) {
if(root -> left == nullptr) return root -> val == 0 ? false : true;
bool left = evaluateTree(root -> left);
bool right = evaluateTree(root -> right);
return root -> val == 2 ? left | right : left & right;
}
};
2 求根节点到叶节点数字之和
2.1 题目链接
2.2 题目描述
给你一个二叉树的根节点 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
提示:
- 树中节点的数目在范围
[1, 1000]内 0 <= Node.val <= 9- 树的深度不超过
10
2.3 解法(dfs - 前序遍历):
前序遍历按照根节点、左⼦树、右⼦树的顺序遍历⼆叉树的所有节点,通常⽤于⼦节点的状态依赖于⽗节点状态的题⽬。
算法思路:
在前序遍历的过程中,我们可以往左右⼦树传递信息,并且在回溯时得到左右⼦树的返回值。递归函数可以帮我们完成两件事:
- 将⽗节点的数字与当前节点的信息整合到⼀起,计算出当前节点的数字,然后传递到下⼀层进⾏递归;
- 当遇到叶⼦节点的时候,就不再向下传递信息,而是将整合的结果向上⼀直回溯到根节点。
在递归结束时,根节点需要返回的值也就被更新为了整棵树的数字和。
算法流程:
递归函数设计:int dfs(TreeNode* root, int num)
- 返回值:当前⼦树计算的结果(数字和);
- 参数 num:递归过程中往下传递的信息(⽗节点的数字);
- 函数作⽤:整合⽗节点的信息与当前节点的信息计算当前节点数字,并向下传递,在回溯时返回当前⼦树(当前节点作为⼦树根节点) 数字和。
递归函数流程:
- 当遇到空节点的时候,说明这条路从根节点开始没有分⽀,返回 0;
- 结合⽗节点传下的信息以及当前节点的 val,计算出当前节点数字 sum;
- 如果当前结点是叶⼦节点,直接返回整合后的结果 sum;
- 如果当前结点不是叶⼦节点,将 sum 传到左右⼦树中去,得到左右⼦树中节点路径的数字和,然后相加后返回结果。
2.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sumNumbers(TreeNode* root) {
return dfs(root, 0);
}
int dfs(TreeNode* root, int presum)
{
presum = presum * 10 + root -> val;
if(root -> left == nullptr && root ->right == nullptr)
return presum;
int ret = 0;
if(root -> left) ret += dfs(root -> left, presum);
if(root -> right) ret += dfs(root -> right, presum);
return ret;
}
};
3 二叉树剪枝
3.1 题目链接
3.2 题目描述
给你二叉树的根结点 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]
提示:
- 树中节点的数目在范围
[1, 200]内 Node.val为0或1
3.3 解法(dfs - 后序遍历):
后序遍历按照左⼦树、右⼦树、根节点的顺序遍历⼆叉树的所有节点,通常⽤于⽗节点的状态依赖于⼦节点状态的题⽬。
算法思路:
如果我们选择从上往下删除,我们需要收集左右⼦树的信息,这可能导致代码编写相对困难。然⽽,通过观察我们可以发现,如果我们先删除最底部的叶⼦节点,然后再处理删除后的节点,最终的结果并不会受到影响。
因此,我们可以采⽤后序遍历的⽅式来解决这个问题。在后序遍历中,我们先处理左⼦树,然后处理右⼦树,最后再处理当前节点。在处理当前节点时,我们可以判断其是否为叶⼦节点且其值是否为 0,如果满⾜条件,我们可以删除当前节点。
- 需要注意的是,在删除叶⼦节点时,其⽗节点很可能会成为新的叶⼦节点。因此,在处理完⼦节点后,我们仍然需要处理当前节点。这也是为什么选择后序遍历的原因(后序遍历⾸先遍历到的⼀定是叶⼦节点)。
- 通过使⽤后序遍历,我们可以逐步删除叶⼦节点,并且保证删除后的节点仍然满⾜删除操作的要求。这样,我们可以较为⽅便地实现删除操作,⽽不会影响最终的结果。
- 若在处理结束后所有叶⼦节点的值均为 1,则所有⼦树均包含 1,此时可以返回。
算法流程:
递归函数设计:void dfs(TreeNode*& root)
- 返回值:⽆;
- 参数 :当前需要处理的节点;
- 函数作⽤:判断当前节点是否需要删除,若需要删除,则删除当前节点。
后序遍历的主要流程:
- 递归出⼝:当传⼊节点为空时,不做任何处理;
- 递归处理左⼦树;
- 递归处理右⼦树;
- 处理当前节点:判断该节点是否为叶⼦节点(即左右⼦节点均被删除,当前节点成为叶⼦节点),并且节点的值为 0:
- a. 如果是,就删除掉;
- b. 如果不是,就不做任何处理。
3.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
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)
root = nullptr;
return root;
}
};
4 验证二叉搜索树
4.1 题目链接
4.2 题目描述
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
-
节点的左
子树
只包含 小于 当前节点的数。
-
节点的右子树只包含 大于 当前节点的数。
-
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入: root = [2,1,3]
输出: true
示例 2:
输入: root = [5,1,4,null,null,3,6]
输出: false
解释: 根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 104]内 -231 <= Node.val <= 231 - 1
4.3 解法(利⽤中序遍历):
后序遍历按照左⼦树、根节点、右⼦树的顺序遍历⼆叉树的所有节点,通常⽤于⼆叉搜索树相关题⽬。
算法思路:
如果⼀棵树是⼆叉搜索树,那么它的中序遍历的结果⼀定是⼀个严格递增的序列。
因此,我们可以初始化⼀个⽆穷⼩的全区变量,⽤来记录中序遍历过程中的前驱结点。那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,然后修改前驱结点为当前结点,传⼊下⼀层的递归中。
算法流程:
- 初始化⼀个全局的变量 prev,⽤来记录中序遍历过程中的前驱结点的 val;
- 中序遍历的递归函数中:
- a. 设置递归出⼝:root == nullptr 的时候,返回 true;
- b. 先递归判断左⼦树是否是⼆叉搜索树,⽤ retleft 标记;
- c. 然后判断当前结点是否满⾜⼆叉搜索树的性质,⽤ retcur 标记:
- 如果当前结点的 val ⼤于 prev,说明满⾜条件,retcur 改为 true;
- 如果当前结点的 val ⼩于等于 prev,说明不满⾜条件,retcur 改为 false;
- d. 最后递归判断右⼦树是否是⼆叉搜索树,⽤ retright 标记;
- 只有当 retleft、 retcur 和 retright 都是 true 的时候,才返回 true。
4.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
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小的元素
5.1 题目链接
5.2 题目描述
给定一个二叉搜索树的根节点 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
提示:
- 树中的节点数为
n。 1 <= k <= n <= 1040 <= Node.val <= 104
5.3 解法(中序遍历 + 计数器剪枝):
算法思路:
上述解法不仅使⽤⼤量额外空间存储数据,并且会将所有的结点都遍历⼀遍。
但是,我们可以根据中序遍历的过程,只需扫描前 k 个结点即可。
因此,我们可以创建⼀个全局的计数器 count,将其初始化为 k,每遍历⼀个节点就将 count--。直到某次递归的时候,count 的值等于 1,说明此时的结点就是我们要找的结果。
算法流程:
定义⼀个全局的变量 count,在主函数中初始化为 k 的值(不⽤全局也可以,当成参数传⼊递归过程中);
递归函数的设计:int dfs(TreeNode* root):
- 返回值为第 k 个结点;
递归函数流程(中序遍历):
- 递归出⼝:空节点直接返回 -1,说明没有找到;
- 去左⼦树上查找结果,记为 retleft:
- a. 如果 retleft == -1,说明没找到,继续执⾏下⾯逻辑;
- b. 如果 retleft != -1,说明找到了,直接返回结果,⽆需执⾏下⾯代码(剪枝);
- 如果左⼦树没找到,判断当前结点是否符合:
- a. 如果符合,直接返回结果
- 如果当前结点不符合,去右⼦树上寻找结果。
5.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
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 二叉树的所有路径
6.1 题目链接
6.2 题目描述
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入: root = [1,2,3,null,5]
输出: ["1->2->5","1->3"]
示例 2:
输入: root = [1]
输出: ["1"]
提示:
- 树中节点的数目在范围
[1, 100]内 -100 <= Node.val <= 100
6.3 解法(回溯):
算法思路:
使⽤深度优先遍历(DFS)求解。
路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加⼊到路径中,如果该节点为叶⼦节点,将路径存储到结果中。否则,将 "->" 加⼊到路径中并递归遍历该节点的左右⼦树。
定义⼀个结果数组,进⾏递归。递归具体实现⽅法如下:
- 如果当前节点不为空,就将当前节点的值加⼊路径 path 中,否则直接返回;
- 判断当前节点是否为叶⼦节点,如果是,则将当前路径加⼊到所有路径的存储数组 paths 中;
- 否则,将当前节点值加上 "->" 作为路径的分隔符,继续递归遍历当前节点的左右⼦节点。
- 返回结果数组。
- 特别地,我们可以只使⽤⼀个字符串存储每个状态的字符串,在递归回溯的过程中,需要将路径中的当前节点移除,以回到上⼀个节点。
具体实现⽅法如下:
- 定义⼀个结果数组和⼀个路径数组。
- 从根节点开始递归,递归函数的参数为当前节点、结果数组和路径数组。
- a. 如果当前节点为空,返回。
- b. 将当前节点的值加⼊到路径数组中。
- c. 如果当前节点为叶⼦节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果数组中。
- d. 递归遍历当前节点的左⼦树。
- e. 递归遍历当前节点的右⼦树。
- f. 回溯,将路径数组中的最后⼀个元素移除,以返回到上⼀个节点。
- 返回结果数组。
6.4 C++算法代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<string> ret;
vector<string> binaryTreePaths(TreeNode* root) {
string path;
if(root == nullptr) return ret;
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);
}
};