4.21

257. 二叉树的所有路径 - 力扣(LeetCode)
方法一:递归,路径path为参数
递归二叉树的过程中,额外传入字符串参数 path,初始为空串。
分类讨论:
- 如果当前节点是空节点,什么也不做,返回。
- 否则,先把节点值(字符串形式)加到 path 的末尾。
- 如果当前节点是叶子节点,把 path 加到答案。
- 如果当前节点不是叶子节点,那么后续必然还会在 path 后加入新的节点值。在此之前,把 -> 加到 path 的末尾。
class Solution { public: vector<string> binaryTreePaths(TreeNode* root) { vector<string> res; auto dfs = [&](this auto&& dfs , TreeNode* node , string path)->void{ if(!node) return; path += to_string(node->val); if(node->left == node->right){ res.push_back(path); return; } path += "->"; dfs(node->left , path); dfs(node->right , path); }; dfs(root , ""); return res; } };细节:
path += to_string(node->val);不可写成path += node->val否则会生成乱码错误
path += "->";这里->要用""双引号包裹,如果用单引号,出现以下错误
方法二:回溯,路径为外部变量
把 path 声明为 DFS 外的变量。
在这个递归过程中:
- 如果没有递归到叶子节点,我们会先递归左子树,然后递归右子树。
- 递归完了左子树,就要倒回去,递归右子树。
- 倒回去的过程中,之前加到 path 中的数据(在左子树中)是垃圾数据,要及时清除掉(恢复现场)。
class Solution { public: vector<string> binaryTreePaths(TreeNode* root) { vector<string> res; vector<string> path;//注意path是字符串数组 auto dfs = [&](this auto&& dfs , TreeNode* node)->void{ if(!node) return; path.push_back(to_string(node->val)); if(node->left == node->right){//叶子节点就拼接答案 string joined_path; for(int i = 0 ; i < path.size() ; i ++){ if(i > 0) joined_path += "->"; joined_path += path[i]; } res.push_back(joined_path); } dfs(node->left); dfs(node->right); path.pop_back(); }; dfs(root); return res; } };细节:
- vector
path; path是字符串数组,如果声明为string,一个二位数可能被拆成许多位:
递归过程的理解
问: “递归完左子树,就要倒回去,递归右子树”,那为什么不是在
dfs(node.right)之前就需要恢复现场呢?答: 记住一点:在哪个递归中 push,就在哪个递归中 pop。“自己做的事自己解决”
LCR 143. 子结构判断 - 力扣(LeetCode)
匹配类二叉树可以使用一种套路相对固定的递归函数,在周赛中和每日一题中多次出现,而第一次见到不太容易写出正确的递归解法,因此我们来总结一下。
这类题目与字符串匹配有些神似,求解过程大致分为两步:
先将根节点匹配;
根节点匹配后,对子树进行匹配。
而参与匹配的二叉树可以是一棵,与自身匹配;也可以是两棵,即互相匹配。
这道题的题意是这样的:输入两棵二叉树
A和B,判断B是不是A的子结构,且约定空树不是任意一个树的子结构。
比如上面这个例子,我们发现
B是A的子结构,因为它们的结构相同,且节点值相等。只要B树走完了,就可以返回true(不管4的右子树2)求解思路可以分解为以下两步:
匹配根节点:首先在
A中找到与B的根节点匹配的节点C;匹配其他节点:验证
C的子树与B的子树是否匹配。
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
if(A == nullptr || B == nullptr) return false;
auto dfs = [&](this auto&& dfs , TreeNode* A , TreeNode* B)->bool{
if(B == nullptr) return true;//只要B树走完了,就可以返回true
if(A == nullptr) return false;
return (A->val == B ->val) && dfs(A->left , B->left) && dfs(A->right , B->right);
};
return dfs(A , B) || isSubStructure(A->left , B) || isSubStructure(A->right , B);
}
};
总结
熟悉了以上的思路之后,力扣很多类似的题目都可以使用题目中的代码解决。
这些题目中,可能会有自身和自身做匹配的,比如每日一题 「101. 对称二叉树」,将自身看作两棵树,用左子树和右子树镜像比较;
可能会将另一棵树变成一个链表,比如 「1367. 二叉树中的列表」,仍然是先将链表头部与二叉树的某个节点匹配,再验证后续是否匹配;
还有与例题相同思路的 「572. 另一个树的子树」
类似题:572. 另一棵树的子树 - 力扣(LeetCode)
本题要求子树结构完全一样,因此在上题的代码上稍加修改:
- 只有两者同时走到空才返回true
- 一者走到空就返回false
if(A == nullptr && B == nullptr) return true; if(A == nullptr || B == nullptr) return false;
class Solution {
public:
bool isSubtree(TreeNode* A, TreeNode* B) {
if(A == nullptr || B == nullptr) return false;
auto dfs = [&](this auto&& dfs , TreeNode* A , TreeNode* B)->bool{
if(A == nullptr && B == nullptr) return true;
if(A == nullptr || B == nullptr) return false;
return (A->val == B ->val) && dfs(A->left , B->left) && dfs(A->right , B->right);
};
return dfs(A , B) || isSubtree(A->left , B) || isSubtree(A->right , B);
}
};
101. 对称二叉树 - 力扣(LeetCode)
class Solution {
public:
bool isSymmetric(TreeNode* root) {
auto check = [&](this auto&& check , TreeNode* l , TreeNode* r)->bool{
if(l == nullptr && r == nullptr) return true;
if(l == nullptr || r == nullptr) return false;
return l->val == r->val && check(l->left , r->right) && check(l->right , r->left);
};
return check(root->left , root->right);
}
};
更简单的写法:将这两行合并
if(l == nullptr && r == nullptr) return true;
if(l == nullptr || r == nullptr) return false;
// if(!l || !r) return l == r;
class Solution {
bool check(TreeNode* p , TreeNode* q){
if(!p || !q) return p == q;
return p->val == q->val && check(p->left , q->right) && check(p->right , q->left);
}
public:
bool isSymmetric(TreeNode* root) {
return check(root->left , root->right);
}
};
1367. 二叉树中的链表 - 力扣(LeetCode)
class Solution {
public:
bool isSubPath(ListNode* head, TreeNode* root) {
if(head == nullptr || root == nullptr) return false;
auto dfs = [&](this auto&& dfs , TreeNode* node , ListNode* head)->bool{
if(head == nullptr) return true;
if(node == nullptr) return false;
return (head->val == node->val) && (dfs(node->left , head->next) || dfs(node->right , head->next));
};
return dfs(root, head) || isSubPath(head,root->left) || isSubPath(head , root->right);
}
};
代码细节:
最后一行不能写成:
return dfs(root, head) || dfs(head,root->left) || dfs(head , root->right);
dfs(root->left, head)仅检查以root->left节点为起点的路径是否匹配链表。- 但实际需要的是递归检查 整个左子树的所有节点 作为起点的可能性(包括
root->left->left,root->left->right等更深层的节点),而不仅仅是root->left这一个节点。原始代码中
return dfs(root, head) || isSubPath(head, root->left) || isSubPath(head, root->right);的合理性在于:
isSubPath(head, root->left)会递归处理整个左子树,检查左子树中 所有节点 作为起点的可能性(包括root->left、root->left->left、root->left->right等)。- 同理,
isSubPath(head, root->right)会处理整个右子树。
29. 两数相除 - 力扣(LeetCode)
在解决LeetCode的两数相除问题时,关键在于高效地使用位运算来模拟除法过程,并处理所有可能的边界情况,例如溢出。以下是分步解决方案:
方法思路
- 处理特殊情况:当除数为1或-1时,直接处理结果以避免不必要的计算。
- 确定结果的符号:根据被除数和除数的符号是否相同确定结果的符号。
- 转换为负数处理:将除数和被除数都转换为负数以避免溢出问题,特别是在处理
-2147483648时。- 位运算加速:通过不断将除数左移(即翻倍)来逼近被除数,从而快速减少迭代次数。
- 处理溢出:在返回结果前检查是否超出32位有符号整数范围。
解决代码
class Solution { public: int divide(int dividend, int divisor) { // 处理除数为1和-1的特殊情况 if (divisor == 1) return dividend; else if (divisor == -1) { if (dividend == INT_MIN) { return INT_MAX; // 溢出情况处理 } return -dividend; } // 确定结果的符号 bool positive = (dividend < 0) == (divisor < 0); // 将被除数和除数转换为负数以避免溢出(-2^31 ~ 2^31 - 1) if (dividend > 0) { dividend = -dividend; } if (divisor > 0) { divisor = -divisor; } int res = 0; while (dividend <= divisor) {//被除数的绝对值大于除数绝对值时 int current_divisor = divisor; int current_count = 1; // 使用位运算快速找到最大的除数倍数 while (current_divisor >= (INT_MIN >> 1) && (current_divisor + current_divisor) >= dividend) { current_divisor += current_divisor; current_count += current_count; } dividend -= current_divisor; res += current_count; } // 处理结果溢出 if (positive) { return res > INT_MAX ? INT_MAX : res; } else { // 防止当res为INT_MIN时,-res溢出 return res == INT_MIN ? INT_MIN : -res; } } };代码解释
整理了一下思路,可以简单概括为:
60/8 = (60-32)/8 + 4 = (60-32-16)/8 + 2 + 4 = 1 + 2 + 4 = 7以下是使用示例 dividend = -10(绝对值10),divisor = -3(绝对值3) 的分步解释:
外层循环逻辑
while (dividend <= divisor) { // 当被除数绝对值 >= 除数绝对值时循环 int current_divisor = divisor; // 当前除数(初始为-3) int current_count = 1; // 当前倍数(初始为1) // 位运算加速:找到最大的除数倍数 while ( current_divisor >= (INT_MIN >> 1) && // 防止溢出 (current_divisor + current_divisor) >= dividend // 保证翻倍后仍 <= 被除数绝对值 ) { current_divisor += current_divisor; // 左移一位(翻倍) current_count += current_count; // 倍数同步翻倍 } dividend -= current_divisor; // 减去已计算的部分 res += current_count; // 累计结果 }
分步执行过程
初始状态
dividend = -10(绝对值10)divisor = -3(绝对值3)res = 0
第一次外层循环
- 进入条件:
-10 <= -3(绝对值10 >= 3),成立。- 初始化:
current_divisor = -3current_count = 1- 内层循环:
- 第一次内层循环:
- 检查
current_divisor >= (INT_MIN >> 1)(-3 >= -1073741824),成立。- 检查
current_divisor*2 >= dividend(-6 >= -10),成立。- 更新:
current_divisor = -6,current_count = 2- 第二次内层循环:
- 检查
current_divisor >= (INT_MIN >> 1)(-6 >= -1073741824),成立。- 检查
current_divisor*2 >= dividend(-12 >= -10),不成立(绝对值12 > 10)。- 退出内层循环。
- 更新外层状态:
dividend = -10 - (-6) = -4(剩余部分)res = 0 + 2 = 2
第二次外层循环
- 进入条件:
-4 <= -3(绝对值4 >= 3),成立。- 初始化:
current_divisor = -3current_count = 1- 内层循环:
- 检查
current_divisor*2 >= dividend(-6 >= -4),不成立(绝对值6 > 4)。- 直接退出内层循环。
- 更新外层状态:
dividend = -4 - (-3) = -1(剩余部分)res = 2 + 1 = 3
第三次外层循环
- 进入条件:
-1 <= -3(绝对值1 < 3),不成立。- 退出循环,最终结果
res = 3。代码通过位运算高效地模拟除法,时间复杂度为 O(log n)。






浙公网安备 33010602011771号