4.21

img

257. 二叉树的所有路径 - 力扣(LeetCode)

方法一:递归,路径path为参数

递归二叉树的过程中,额外传入字符串参数 path,初始为空串。

分类讨论:

  1. 如果当前节点是空节点,什么也不做,返回。
  2. 否则,先把节点值(字符串形式)加到 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;
      }
  };

细节:

  1. path += to_string(node->val);不可写成path += node->val否则会生成乱码错误

image-20250421145215300

  1. path += "->";这里->要用""双引号包裹,如果用单引号,出现以下错误

    image-20250421145333984

方法二:回溯,路径为外部变量

把 path 声明为 DFS 外的变量。

在这个递归过程中:

  1. 如果没有递归到叶子节点,我们会先递归左子树,然后递归右子树。
  2. 递归完了左子树,就要倒回去,递归右子树。
  3. 倒回去的过程中,之前加到 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;
      }
  };

细节:

  1. vector path; path是字符串数组,如果声明为string,一个二位数可能被拆成许多位:

image-20250421151026558

  1. 递归过程的理解

    问: “递归完左子树,就要倒回去,递归右子树”,那为什么不是在dfs(node.right)之前就需要恢复现场呢?

    答: 记住一点:在哪个递归中 push,就在哪个递归中 pop。“自己做的事自己解决”


LCR 143. 子结构判断 - 力扣(LeetCode)

匹配类二叉树可以使用一种套路相对固定的递归函数,在周赛中和每日一题中多次出现,而第一次见到不太容易写出正确的递归解法,因此我们来总结一下。

这类题目与字符串匹配有些神似,求解过程大致分为两步:

  • 先将根节点匹配;

  • 根节点匹配后,对子树进行匹配。

而参与匹配的二叉树可以是一棵,与自身匹配;也可以是两棵,即互相匹配。

这道题的题意是这样的:输入两棵二叉树 AB,判断 B 是不是 A 的子结构,且约定空树不是任意一个树的子结构。

比如上面这个例子,我们发现 BA 的子结构,因为它们的结构相同,且节点值相等。只要B树走完了,就可以返回true(不管4的右子树2)

求解思路可以分解为以下两步:

  1. 匹配根节点:首先在 A 中找到与 B 的根节点匹配的节点 C

  2. 匹配其他节点:验证 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;

image-20250421154836270

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->leftroot->left->leftroot->left->right 等)。
  • 同理,isSubPath(head, root->right) 会处理整个右子树。

29. 两数相除 - 力扣(LeetCode)

在解决LeetCode的两数相除问题时,关键在于高效地使用位运算来模拟除法过程,并处理所有可能的边界情况,例如溢出。以下是分步解决方案:

方法思路

  1. 处理特殊情况:当除数为1或-1时,直接处理结果以避免不必要的计算。
  2. 确定结果的符号:根据被除数和除数的符号是否相同确定结果的符号。
  3. 转换为负数处理:将除数和被除数都转换为负数以避免溢出问题,特别是在处理-2147483648时。
  4. 位运算加速:通过不断将除数左移(即翻倍)来逼近被除数,从而快速减少迭代次数。
  5. 处理溢出:在返回结果前检查是否超出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;
        }
    }
};

代码解释

  1. 整理了一下思路,可以简单概括为:
    60/8 = (60-32)/8 + 4 = (60-32-16)/8 + 2 + 4 = 1 + 2 + 4 = 7

  2. 以下是使用示例 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

第一次外层循环

  1. 进入条件-10 <= -3(绝对值10 >= 3),成立。
  2. 初始化
    • current_divisor = -3
    • current_count = 1
  3. 内层循环
    • 第一次内层循环
      • 检查 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)。
      • 退出内层循环。
  4. 更新外层状态
    • dividend = -10 - (-6) = -4(剩余部分)
    • res = 0 + 2 = 2

第二次外层循环

  1. 进入条件-4 <= -3(绝对值4 >= 3),成立。
  2. 初始化
    • current_divisor = -3
    • current_count = 1
  3. 内层循环
    • 检查 current_divisor*2 >= dividend(-6 >= -4),不成立(绝对值6 > 4)。
    • 直接退出内层循环。
  4. 更新外层状态
    • dividend = -4 - (-3) = -1(剩余部分)
    • res = 2 + 1 = 3

第三次外层循环

  1. 进入条件-1 <= -3(绝对值1 < 3),不成立。
  2. 退出循环,最终结果 res = 3

代码通过位运算高效地模拟除法,时间复杂度为 O(log n)

posted @ 2025-04-22 21:10  七龙猪  阅读(1)  评论(0)    收藏  举报
-->