深度优先遍历(DFS,全称为 Depth First Traversal),是我们树或者图这样的数据结构中常⽤的⼀种遍历算法。这个算法会尽可能深的搜索树或者图的分⽀,直到⼀条路径上的所有节点都被遍历完毕,然后再回溯到上⼀层,继续找⼀条路遍历。
在⼆叉树中,常⻅的深度优先遍历为:前序遍历、中序遍历以及后序遍历。因为树的定义本⾝就是递归定义,因此采⽤递归的⽅法去实现树的三种遍历不仅容易理解⽽且代码很简洁。
并且前中后序三种遍历的唯⼀区别就是访问根节点的时机不同,在做题的时候,选择⼀个适当的遍历顺序,对于算法的理解是⾮常有帮助的。
题目链接:
计算布尔二叉树的值
要点:
2
对应 OR,3
对应 AND。老师代码:
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;
}
}
老师思路:
我的代码:
/**
* 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;
bool cl = evaluateTree(root->left);
bool cr = evaluateTree(root->right);
if(root->val == 2) return cl || cr;
else return cl && cr;
}
};
我的笔记:
||
和 &&
简化逻辑判断,避免冗余条件分支。题目链接:
求根节点到叶节点数字之和
要点:
presum
通过 presum * 10 + root->val
传递给子节点。presum
加入最终结果。老师代码:
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;
}
}
老师思路:
前序遍历按照根节点、左⼦树、右⼦树的顺序遍历⼆叉树的所有节点,通常⽤于⼦节点的状态依赖于⽗节点状态的题⽬。
算法思路:
在前序遍历的过程中,我们可以往左右⼦树传递信息,并且在回溯时得到左右⼦树的返回值。
递归函数可以帮我们完成两件事:
算法流程:递归函数设计:int dfs(TreeNode* root, int num)
返回值:当前⼦树计算的结果(数字和);
参数 num:递归过程中往下传递的信息(⽗节点的数字);
函数作⽤:整合⽗节点的信息与当前节点的信息计算当前节点数字,并向下传递,在回溯时返回当前⼦树(当前节点作为⼦树根节点)数字和。
递归函数流程:
我的代码:
/**
* 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) {
if(root->left == nullptr && root->right == nullptr) return root->val;
int ret = 0;
dfs(root, 0, ret);
return ret;
}
void dfs(TreeNode* root, int sum, int& ret)
{
sum = sum * 10 + root->val;
if(root->left == nullptr && root->right == nullptr)
{
ret += sum;
return;
}
if(root->left) dfs(root->left, sum, ret);
if(root->right) dfs(root->right, sum, ret);
}
};
我的思路:
我的笔记:
ret
题目链接:
二叉树剪枝
要点:
nullptr
。老师代码:
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;
}
}
老师思路:
后序遍历按照左⼦树、右⼦树、根节点的顺序遍历⼆叉树的所有节点,通常⽤于⽗节点的状态依赖于⼦节点状态的题⽬。
算法思路:
如果我们选择从上往下删除,我们需要收集左右⼦树的信息,这可能导致代码编写相对困难。然⽽,通过观察我们可以发现,如果我们先删除最底部的叶⼦节点,然后再处理删除后的节点,最终的结果并不会受到影响。
因此,我们可以采⽤后序遍历的⽅式来解决这个问题。在后序遍历中,我们先处理左⼦树,然后处理右⼦树,最后再处理当前节点。在处理当前节点时,我们可以判断其是否为叶⼦节点且其值是否为 0,如果满⾜条件,我们可以删除当前节点。
需要注意的是,在删除叶⼦节点时,其⽗节点很可能会成为新的叶⼦节点。因此,在处理完⼦节点后,我们仍然需要处理当前节点。这也是为什么选择后序遍历的原因(后序遍历⾸先遍历到的⼀定是叶⼦节点)。
通过使⽤后序遍历,我们可以逐步删除叶⼦节点,并且保证删除后的节点仍然满⾜删除操作的要求。这样,我们可以较为⽅便地实现删除操作,⽽不会影响最终的结果。
若在处理结束后所有叶⼦节点的值均为 1,则所有⼦树均包含 1,此时可以返回。
我的代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) :o val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNde *right) : val(x), left(left), right(right) {}
* };
*/
class Solution
{
public:
TreeNode* pruneTree(TreeNode* root)
{
return dfs(root);
}
TreeNode* dfs(TreeNode* root)
{
if(root->left == nullptr && root->right == nullptr)//访问到叶子结点
{
if(root->val == 0) return nullptr;//如果父节点为0返回空指针
else return root;//否则返回自身
}
TreeNode* t1 = nullptr, *t2 = nullptr;//必须要先初始化为空指针
//后序遍历
if(root->left) t1 = dfs(root->left);
if(root->right) t2 = dfs(root->right);
//更新当前节点指向的左右子树
root->left = t1;
root->right = t2;
//判断当前节点是否需要剪枝
if(t1 == nullptr && t2 == nullptr)
{
if(root->val == 0) return nullptr;
else return root;
}
else
return root;
}
};
我的思路:
delete
,并置空指针,代码更规范。我的笔记:
题目链接:
验证二叉搜索树
要点:
long prev
保存中序遍历的前驱节点值。false
终止递归。老师代码:
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;
}
}
老师思路:
如果⼀棵树是⼆叉搜索树,那么它的中序遍历的结果⼀定是⼀个严格递增的序列。因此,我们可以初始化⼀个⽆穷⼩的全区变量,⽤来记录中序遍历过程中的前驱结点。那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,然后修改前驱结点为当前结点,传⼊下⼀层的递归中。
初始化⼀个全局的变量 prev,⽤来记录中序遍历过程中的前驱结点的 val;
中序遍历的递归函数中:
a. 设置递归出⼝:root == nullptr 的时候,返回 true;
b. 先递归判断左⼦树是否是⼆叉搜索树,⽤ retleft 标记;
c. 然后判断当前结点是否满⾜⼆叉搜索树的性质,⽤ retcur 标记:
d. 最后递归判断右⼦树是否是⼆叉搜索树,⽤ retright 标记; 3. 只有当 retleft、 retcur 和 retright 都是 true 的时候,才返回 true。
我的代码:
/**
* 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 isValidBST(TreeNode* root)
{
return dfs(root, root->val, root->val);
}
bool dfs(TreeNode* root, int& min, int& max)
{
//如果为叶子结点,最大值和最小值都是它本身
if(root->left == nullptr && root->right == nullptr)
{
min = max = root->val;
return true;
}
//后序遍历,初始化一定要给一个值,就是它本身
bool b1 = true, b2 = true;
int lmin = root->val, lmax = root->val, rmin = root->val, rmax = root->val;
if(root->left)
{
b1 = dfs(root->left, lmin, lmax);
if(lmax >= root->val) return false;//左子树的最大值大于当前节点,不是搜索二叉树
}
if(root->right)
{
b2 = dfs(root->right, rmin, rmax);
if(rmin <= root->val) return false;//右子树的最小值小于当前节点,不是搜索二叉树
}
//更新当前搜索树的最大值和最小值
min = lmin, max = rmax;
return b1 && b2;
}
};
解法二:
/**
* 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:
long prev = LONG_MIN;
bool isValidBST(TreeNode* root)
{
if(root->left == nullptr && root->right == nullptr)
{
if(root->val > prev)
{
prev = root->val;
return true;
}
else return false;
}
int b1 = true, b2 = true;
if(root->left) b1 = isValidBST(root->left);//判断左子树
if(!b1) return false;//剪枝,无需再递归后面的子树了
if(root->val <= prev) return false;//判断当前节点是否满足
prev = root->val;
if(root->right) b2 = isValidBST(root->right);//判断右子树
return b2;
}
};
我的思路:
prev
简化递归参数传递。我的笔记:
老师用到的方法:二叉搜索树的中序遍历的结果,是一个有序的序列
边界值处理:使用 LONG_MIN
避免 INT_MIN
的边界问题。
递归展开图:画图中序遍历过程,理解 prev
的更新逻辑
全局变量的优势:如果在递归中能够想到使用全局变量,此时我们就不需要处理递归中的参数和返回值,能简化递归的过程
回溯和剪枝
回溯在递归中经常出现,当我们完成子树的递归之后返回上一级的递归时就会出现回溯
剪枝其实是一个加快搜索的过程:当我们发现一个递归(二叉树)的分支绝对不可能出现我们想要的结果的时候,我们就不需要再对那个分支进行递归了,这就是剪枝
题目链接:
二叉搜索树中第K小的元素
要点:
count
递减计数,找到第k小元素后终止递归。老师代码:
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);
}
}
老师思路:
我们可以根据中序遍历的过程,只需扫描前 k 个结点即可。 因此,我们可以创建⼀个全局的计数器 count,将其初始化为 k,每遍历⼀个节点就将 count–。直到某次递归的时候,count 的值等于 1,说明此时的结点就是我们要找的结果。
递归函数流程(中序遍历):
递归出⼝:空节点直接返回 -1,说明没有找到;
去左⼦树上查找结果,记为 retleft:
a. 如果 retleft == -1,说明没找到,继续执⾏下⾯逻辑;
b. 如果 retleft != -1,说明找到了,直接返回结果,⽆需执⾏下⾯代码(剪枝);
如果左⼦树没找到,判断当前结点是否符合:
a. 如果符合,直接返回结果
如果当前结点不符合,去右⼦树上寻找结果
我的代码:
/**
* 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 _k = 0;
int _num = 0;
int ret = 0;
public:
int kthSmallest(TreeNode* root, int k) {
_k = k;
dfs(root);
return ret;
}
void dfs(TreeNode* root)
{
if(root == nullptr) return;
dfs(root->left);
_num++;
if(_k == _num)
{
ret = root->val;
return;
}
dfs(root->right);
}
};
我的思路:
写的挺差的
我的笔记:
_num
计数,但未及时剪枝,可能遍历多余节点;count == 0
时直接终止递归,效率更高。题目链接:
二叉树的所有路径
要点:
老师代码:
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);
}
};
老师思路:
使⽤深度优先遍历(DFS)求解。
路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加⼊到路径中,
如果该节点为叶⼦节点,将路径存储到结果中。
否则,将 “->” 加⼊到路径中并递归遍历该节点的左右⼦树。
定义⼀个结果数组,进⾏递归。
递归具体实现⽅法如下:
具体实现⽅法如下:
定义⼀个结果数组和⼀个路径数组。
从根节点开始递归,递归函数的参数为当前节点、结果数组和路径数组。
a. 如果当前节点为空,返回。
b. 将当前节点的值加⼊到路径数组中。
c. 如果当前节点为叶⼦节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果数组中。
d. 递归遍历当前节点的左⼦树。
e. 递归遍历当前节点的右⼦树。
f. 回溯,将路径数组中的最后⼀个元素移除,以返回到上⼀个节点。
返回结果数组。
我的代码:
/**
* 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> binaryTreePaths(TreeNode* root)
{
vector<string> vs;
dfs(root, {}, vs);
return vs;
}
void dfs(TreeNode* root, string s, vector<string>& vs)
{
if(root->left == nullptr && root->right == nullptr)
{
s += to_string(root->val);
vs.push_back(s);
return;
}
s += to_string(root->val);
s += "->";
if(root->left) dfs(root->left, s, vs);
if(root->right) dfs(root->right, s, vs);
}
};
我的笔记:
std::stringstream
优化)。在这个题中我们需要明白回溯中的一个概念:恢复现场
老师在将这个题的时候说如果我们把路径字符path
串定义为全局变量就一定要恢复现场,因为当我们递归子问题之后回到上一级递归函数的时候如果路径字符串path
是全局变量就会改变它的值,这样在传入下一个子问题的递归函数的时候就会出现多了字符串的情况
最好的解决办法就是把路径字符串定义在函数头作为一个函数形参进行传递,这样就避免了复杂的恢复现场的操作