代码随想录算法训练营 Day14 | 二叉树 part04

513. 找树左下角的值

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

cpp 复制代码
class Solution {
public:
    int max_depth; // 记录全局最大深度
    int ans;       // 记录最终结果(最底层的最左侧节点值)
    // DFS 函数
    // root: 当前节点
    // depth: 当前节点所在的深度
    void dfs(TreeNode* root, int depth) {
        if (root == NULL) return;
        // 1. 判断是否为叶子节点
        if (!root->left && !root->right) {
            // 【核心逻辑】
            // 如果当前深度超过了之前记录的最大深度,说明找到了"更深"的一层
            // 因为是前序遍历(先左后右),所以同一层中第一个被访问到的叶子节点一定是最左边的
            if (depth > max_depth) {
                max_depth = depth; // 更新最大深度
                ans = root->val;   // 更新答案
            }
        }
        // 2. 递归左右子树
        // 注意顺序:必须先递归左子树,再递归右子树
        // 这样才能保证在"同一深度"时,先访问到左边的节点
        dfs(root->left, depth + 1);
        dfs(root->right, depth + 1);
    }
    int findBottomLeftValue(TreeNode* root) {
        max_depth = -1; // 初始化为 -1,确保根节点(深度0)能被记录
        ans = 0;
        dfs(root, 0);   // 根节点深度从 0 开始
        return ans;
    }
};

总结

1. 解题思路:DFS + 前序遍历
  1. 最底层:通过 depth > max_depth 来保证每次更新都是找到了更深的一层。
  2. 最左侧:通过 前序遍历(根左右) 的顺序来保证。

为什么前序遍历能保证是"最左侧"?

  • 因为我们在递归时,先调用 dfs(root->left),后调用 dfs(root->right)
  • 当 DFS 到达某一层时,最先遇到 的叶子节点一定是该层最左边的节点。
  • 由于判断条件是 depth > max_depth(严格大于),所以同一层后续遇到的节点(中间或右侧)会被忽略。
2. 方法对比:DFS vs BFS
  • DFS(本代码):
    • 优点:代码简洁,利用递归栈。
    • 思路:一直往下挖,挖到底比深度,利用遍历顺序确定左右。
  • BFS(层序遍历):
    • 思路:一层一层遍历,直接记录每一层的第一个节点。遍历到最后一层时,第一个节点即为答案。
    • 优点:直观,天然符合"找底层"和"找左边"的逻辑。
    • 缺点:需要借助队列 queue,空间复杂度取决于树的最大宽度。
3. 复杂度分析
  • 时间复杂度:O(N)
    • 每个节点只访问一次。
  • 空间复杂度:O(N)
    • 取决于递归栈的深度。最坏情况(链状树)为 O(N)。

112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

叶子节点 是指没有子节点的节点。

cpp 复制代码
class Solution {
public:
    // dfs 函数:判断从当前节点出发,是否能凑出剩余的和 sum
    // 注意:这里 sum 是值传递,不是引用
    bool dfs(TreeNode* root, int sum) {
        // 1. 进入节点时,先减去当前节点的值
        sum -= root->val;
        // 2. 终止条件:遇到叶子节点(左右孩子都为空)
        if (root->left == NULL && root->right == NULL) {
            return sum == 0; // 如果剩余和刚好为0,说明路径找到了
        }
        // 3. 递归左子树
        if (root->left) {
            // 如果左子树能找到路径,直接返回 true
            if (dfs(root->left, sum)) return true;
        }
        // 4. 递归右子树
        if (root->right) {
            // 如果右子树能找到路径,直接返回 true
            if (dfs(root->right, sum)) return true;
        }
        // 5. 左右子树都没找到,返回 false
        return false;
    }
    bool hasPathSum(TreeNode* root, int targetSum) {
        // 特殊情况:空树不存在路径
        if (root == NULL) return false;
        return dfs(root, targetSum);
    }
};

总结

1. 核心技巧:值传递代替显式回溯

与之前的"二叉树路径"题目类似,这里使用了 int sum 进行值传递:

  • 原理:sum -= root->val 这行代码只在当前函数作用域生效。当递归进入 dfs(root->left, sum) 时,传递的是减完之后的副本。
  • 回溯:当左子树递归结束返回到当前层时,当前的 sum 变量并没有变(因为没传引用),所以不需要写 sum += root->val 这种回溯代码,直接把当前的 sum 传给右子树即可。
2. 逻辑细节
  • 先减后判:先减去当前节点值,再判断是否为叶子节点。这个顺序很重要,保证了叶子节点本身的值也被算进总和。
  • 短路逻辑:if (dfs(...)) return true; 这是一个很好的优化。一旦左子树找到了路径,就不会再去递归右子树,直接返回结果。
3. 复杂度分析
  • 时间复杂度:O(N)
    • 最坏情况下需要遍历所有节点。
  • 空间复杂度:O(N)
    • 取决于递归栈深度,最坏情况(链状树)为 O(N)。

113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

cpp 复制代码
class Solution {
public:
    vector<int> path;            // 记录当前路径
    vector<vector<int>> ans;     // 记录所有符合条件的路径结果
    void dfs(TreeNode* root, int sum) {
        // 1. 处理当前节点
        // 这两行是"做选择":减去目标值,加入路径
        sum -= root->val;
        path.push_back(root->val);
        // 2. 终止条件:遇到叶子节点
        if (root->left == NULL && root->right == NULL) {
            if (sum == 0) {
                ans.push_back(path); // 找到目标路径,加入结果集
            }
            // 注意:这里不需要 return,也不能 return
            // 让代码流自然向下执行到 pop_back(),完成回溯清理工作
        }
        // 3. 递归左右子树
        if (root->left) dfs(root->left, sum);
        if (root->right) dfs(root->right, sum);
        // 4. 回溯:撤销处理结果
        // 【核心】无论是否找到路径,函数返回前都要将当前节点移出路径
        // 恢复 path 的状态,以便返回上一层后尝试其他分支
        path.pop_back();
    }
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        if (root == NULL) return ans;
        dfs(root, targetSum);
        return ans;
    }
};

总结

1. 递归三部曲:
  • 做选择:sum 减去当前值,当前值加入 path
  • 递归:去左子树找,去右子树找。
  • 撤销选择:path.pop_back(),把刚才加进来的吐出去,回到上一层。
2. 两个关键细节:
  • 为什么叶子节点判断不 return
    如果直接 return,会跳过后面的 pop_back(),导致路径没清理干净。这里不写 return,让代码自然流到下面的 pop_back(),保证每个节点退出前都能清理现场。
  • 为什么 sum 不用撤销?
    因为 sum 是值传递(复制了一份),改了不影响上一层,自动就是"撤销"状态。
3. 复杂度分析
  • 时间复杂度:O(N²)
    • 遍历节点需要 O(N)。
    • 当找到路径存入 ans 时,push_back(path) 会复制一次 vector,最坏路径长度 O(N),故总复杂度 O(N²)。
  • 空间复杂度:O(N)
    • 递归栈深度取决于树的高度,最坏为 O(N)。

106. 从中序与后序遍历序列构造二叉树

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

cpp 复制代码
class Solution {
public:
    // dfs 递归构建函数
    // 参数说明:中序数组、后序数组、当前中序遍历的左右边界、当前后序遍历的左右边界
    TreeNode* build(vector<int>& inorder, vector<int>& postorder, int inleft, int inright, int poleft, int poright) {
        // 1. 终止条件:如果左边界超过右边界,说明该区间没有节点,返回空
        if (inright - inleft < 0) return NULL;
        // 2. 获取根节点的值
        // 后序遍历的最后一个节点一定是当前子树的根节点
        int val = postorder[poright];
        TreeNode* root = new TreeNode(val);
        // 3. 剪枝/优化:如果当前区间只有一个节点,它就是叶子节点,直接返回
        // 这个判断不是必须的,但可以减少不必要的递归调用
        if (inright - inleft == 0) return root;
        // 4. 在中序遍历中寻找根节点的位置,用于切分左右子树
        int index = -1;
        for (int i = inleft; i <= inright; i++) {
            if (inorder[i] == val) {
                index = i;
                break;
            }
        }
        // 5. 递归构建左子树
        // 中序范围:[inleft, index - 1] (根节点左边)
        // 后序范围:[poleft, poleft + (index - 1 - inleft)]
        // 解释:后序左子树的长度 = 中序左子树的长度 (index - inleft)
        //       所以右边界 = poleft + 长度 - 1
        root->left = build(inorder, postorder, inleft, index - 1, poleft, poleft + index - 1 - inleft);
        // 6. 递归构建右子树
        // 中序范围:[index + 1, inright] (根节点右边)
        // 后序范围:[poleft + (index - inleft), poright - 1]
        // 解释:后序右子树的起点 = 左子树起点 + 左子树长度
        //       后序右子树的终点 = poright - 1 (去掉最后的根节点)
        root->right = build(inorder, postorder, index + 1, inright, poleft + index - inleft, poright - 1);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        // 初始调用,覆盖整个数组范围
        return build(inorder, postorder, 0, inorder.size() - 1, 0, postorder.size() - 1);
    }
};

总结

1. 解题思路:分治法
  1. 找根:利用后序遍历特性(最后一个元素是根)确定根节点。
  2. 分割:利用中序遍历特性(根节点左边是左子树,右边是右子树)将数组一分为二。
  3. 递归:对分割后的左右两部分重复上述过程。
2. 最难点:区间边界计算

假设 index 是中序遍历中根节点的下标:

  • 左子树长度 = index - inleft

构建左子树时:

  • 中序区间很直观:[inleft, index - 1]
  • 后序区间:起点不变,终点 = 起点 + 长度 - 1。
    • 对应代码:poleft + (index - inleft) - 1

构建右子树时:

  • 中序区间:[index + 1, inright]
  • 后序区间:终点是 poright - 1(去掉当前根节点),起点 = poleft + 左子树长度。
    • 对应代码:poleft + (index - inleft)
3. 复杂度分析
  • 时间复杂度:O(N²)
    • 每次递归都要遍历中序数组寻找根节点 index,最坏情况是 O(N)。
  • 空间复杂度:O(N)
    • 主要是递归调用栈的开销,最坏情况(树退化为链表)为 O(N)。

相关题

cpp 复制代码
class Solution {
public:
    // dfs 递归构建函数
    // 参数说明:前序数组、中序数组、前序左/右边界、中序左/右边界
    TreeNode* build(vector<int>& preorder, vector<int>& inorder, int prleft, int prright, int inleft, int inright) {
        // 1. 终止条件:区间无效,返回空节点
        if (inright - inleft < 0) return NULL;
        // 2. 获取根节点的值
        // 【关键点】前序遍历的第一个节点(当前区间左边界)一定是根节点
        int val = preorder[prleft];
        TreeNode* root = new TreeNode(val);
        // 3. 剪枝:如果区间只有一个节点,直接返回
        if (inright - inleft == 0) return root;
        // 4. 在中序遍历中寻找根节点的位置,用于切分左右子树
        int index = -1;
        for (int i = inleft; i <= inright; i++) {
            if (inorder[i] == val) {
                index = i;
                break;
            }
        }
        // 5. 递归构建左子树
        // 前序范围:[prleft + 1, prleft + 左子树长度]
        // 解释:去掉根节点后,接下来的 左子树长度 个元素就是左子树的前序序列
        // 中序范围:[inleft, index - 1] (根节点左边)
        root->left = build(preorder, inorder, prleft + 1, prleft + index - inleft, inleft, index - 1);
        // 6. 递归构建右子树
        // 前序范围:紧接左子树之后,直到右边界
        // 解释:左子树起点 + 左子树长度 = 右子树起点
        // 中序范围:[index + 1, inright] (根节点右边)
        root->right = build(preorder, inorder, prleft + index - inleft + 1, prright, index + 1, inright);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        return build(preorder, inorder, 0, preorder.size() - 1, 0, inorder.size() - 1);
    }
};
相关推荐
爱丽_1 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
仍然.2 小时前
算法题目---前缀和
算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
大熊背2 小时前
双目拼接摄像机中简单的亮度差校正原理
人工智能·算法·双目拼接·亮度差消除
CoovallyAIHub2 小时前
AAAI 2026 | 上海AI Lab发布RacketVision,首次为球拍运动标注球拍姿态
深度学习·算法·计算机视觉
大熊背2 小时前
双目拼接摄像机中简单的色差校正原理
人工智能·算法·isppipeline·双目拼接
CoovallyAIHub2 小时前
中文语音识别该用谁?6 个开源模型 + 2 个配套工具,一文理清
深度学习·算法·计算机视觉
会编程的土豆2 小时前
【数据结构与算法】 二叉树做题
开发语言·数据结构·c++·算法