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 + 前序遍历
- 最底层:通过
depth > max_depth来保证每次更新都是找到了更深的一层。 - 最左侧:通过 前序遍历(根左右) 的顺序来保证。
为什么前序遍历能保证是"最左侧"?
- 因为我们在递归时,先调用
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. 从中序与后序遍历序列构造二叉树
给定两个整数数组
inorder和postorder,其中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. 解题思路:分治法
- 找根:利用后序遍历特性(最后一个元素是根)确定根节点。
- 分割:利用中序遍历特性(根节点左边是左子树,右边是右子树)将数组一分为二。
- 递归:对分割后的左右两部分重复上述过程。
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);
}
};