Day 14:二叉树
找树左下角的值
题目链接:513. 找树左下角的值 - 力扣(LeetCode)
文章讲解:代码随想录
视频讲解:怎么找二叉树的左下角? 递归中又带回溯了,怎么办?
题目建议:本题递归偏难,反而迭代简单属于模板题
, 两种方法掌握一下 ;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int findBottomLeftValue(TreeNode root) {
}
}
题目解析
方法一:递归法(DFS)
- 使用
前序遍历(左优先)
确保最先访问最左节点 维护最大深度变量,只在叶子节点更新结果
- 时间复杂度
O(n)
,空间复杂度O(h)
(递归栈)
class Solution {
private TreeNode ansNode;
private int maxDepth = 0;
public int findBottomLeftValue(TreeNode root) {
ansNode = root; // err: 初始化, 避免空指针
dfs(root, 0);
return ansNode.val;
}
private void dfs(TreeNode root, int depth){
if(root.left == null && root.right == null){
// 叶子节点, 进入下一步判断
if(depth > maxDepth){
// 当前深度大于最大深度, 更新最大深度和最终节点
maxDepth = depth;
ansNode = root;
}
// 减少后续同一层、更高层不必要的递归
return;
}
if(root.left != null) dfs(root.left, depth + 1);
if(root.right != null) dfs(root.right, depth + 1);
}
}
方法二:迭代法(BFS)
- 层序遍历天然适合找最后一行
- 每层记录第一个节点,最后保存的就是结果
- 时间复杂度
O(n)
,空间复杂度O(w)
(队列)
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
TreeNode ansNode = null;
while (!queue.isEmpty()) {
int count = queue.size();
ansNode = queue.peek();
while (count > 0) {
TreeNode cur = queue.poll(); // err: 应该在循环内 poll()
if (cur.left != null) queue.add(cur.left);
if (cur.right != null) queue.add(cur.right);
count--;
}
}
return ansNode.val;
}
}
总结
-
递归更简洁但较难理解回溯
-
迭代更直观,推荐掌握模板化写法
-
复杂度对比
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
递归法 | O(n) | O(h) |
迭代法 | O(n) | O(w) |
注:h为树高,w为树最大宽度
路径总和
文章讲解:代码随想录
题目建议:本题又一次涉及到回溯的过程,而且回溯的过程隐藏的还挺深,建议先看视频来理解
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
}
}
题目解析
方法一:递归法(DFS)
- 使用
深度优先搜索(DFS)
遍历所有路径 - 到达
叶子节点
时判断路径和是否等于目标值
- 通过参数传递当前剩余的目标值(targetSum - node.val)
- 时间复杂度
O(n)
,空间复杂度O(h)
(递归栈)
手动恢复现场:
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
// 到达叶子节点时判断是否满足条件
if (root.left == null && root.right == null) {
return targetSum == root.val;
}
// 递归检查左右子树
return hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
}
自动恢复现场:
class Solution {
private int target;
public boolean hasPathSum(TreeNode root, int targetSum) {
target = targetSum;
if (root == null) {
return false;
}
return dfs(root, 0); // 递归当前路径的节点和
}
private boolean dfs(TreeNode root, int sum) {
if(root == null){
return false;
}
sum += root.val;
if (root.left == null && root.right == null && sum == target) {
return true;
}
return dfs(root.left, sum) || dfs(root.right, sum); // err: 不小心写成 &&
}
}
方法二:迭代法(BFS)
- 使用
栈
模拟递归过程 显式记录每个节点的累计路径和
- 适合理解递归的回溯过程
- 时间复杂度
O(n)
,空间复杂度O(n)
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
Deque<Pair<TreeNode, Integer>> stack = new ArrayDeque<>();
stack.push(new Pair<>(root, root.val));
while (!stack.isEmpty()) {
Pair<TreeNode, Integer> node = stack.pop();
TreeNode cur = node.getKey();
int sum = node.getValue();
if (cur.left == null && cur.right == null && sum == targetSum) {
return true;
}
if (cur.right != null) {
stack.push(new Pair<>(cur.right, sum + cur.right.val));
}
if (cur.left != null) {
stack.push(new Pair<>(cur.left, sum + cur.left.val));
}
}
return false;
}
}
总结
返回值选择原则:
当只需要判断是否存在(布尔结果)时,使用返回值
当需要记录所有路径时,不需要返回值(通过参数收集结果)
复杂度对比
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
递归法 | O(n) | O(h) |
迭代法 | O(n) | O(n) |
注:n为节点数,h为树高
路径总和Ⅱ
题目链接:113. 路径总和 II - 力扣(LeetCode)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
}
}
题目解析
方法:递归回溯法
-
核心逻辑:
- 使用
回溯法
遍历所有路径 到达叶子节点时检查路径和是否等于目标值
- 满足条件时
保存当前路径(注意新建列表)
- 使用
-
回溯操作:
添加节点值 → 递归 → 移除节点值(恢复状态)
- 体现在
path.add()
和path.remove()
的配对使用
-
复杂度分析:
- 时间复杂度:
O(n²)
(最坏情况每个路径都符合) - 空间复杂度:
O(n)
(递归栈和路径存储)
- 时间复杂度:
注意事项
- 结果存储时需
new ArrayList<>(path)
避免引用问题 - 先判断节点非空再递归,避免空指针异常
回溯操作要对称
(add/remove 成对出现)
class Solution {
private int target;
private List<List<Integer>> ret;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
target = targetSum;
ret = new ArrayList<>();
List<Integer> path = new ArrayList<>();
dfs(root, 0, path);
return ret;
}
private void dfs(TreeNode root, int sum, List<Integer> path){
if(root == null){
return;
}
sum += root.val;
path.add(root.val);
if(root.left == null && root.right == null && sum == target){
ret.add(new ArrayList<>(path));
// 关键 err: 不能直接 ret.add(path)
// 由于 path 是一个引用,后续对 path 的修改(如path.remove(path.size() - 1))
// 会影响到已经添加到 ret 中的路径。因此,最终ret中的路径都是空的。
}
dfs(root.left, sum, path);
dfs(root.right, sum, path);
path.remove(path.size()-1);
// 易错点: 当对一个节点进行完操作后, 移除这个节点, 而不是递归左右节点后, 分别进行一次移除
}
}
从中序与后序遍历序列构造二叉树
题目链接:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
文章讲解:代码随想录
视频讲解:坑很多!来看看你掉过几次坑 | LeetCode:106.从中序与后序遍历序列构造二叉树
题目建议:本题算是比较难的二叉树题目了,大家先看视频来理解。106.从中序与后序遍历序列构造二叉树
,105. 从前序与中序遍历序列构造二叉树
一起做,思路一样的;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
}
}
题目解析
方法:递归分治法
根据中序和后序遍历序列构造二叉树的方法:
后序数组的最后一个元素
是当前子树的根节点。在中序数组中找到根节点的位置
,将中序数组分为左子树和右子树。根据中序数组的划分,确定后序数组中左子树和右子树的范围
。递归构造
:对左子树和右子树重复上述步骤,直到所有子树都被构造完成。
如果手动操作,可以直接根据这两个序列快速画出二叉树:
-
核心思路:
- 递归出口
if(iStart > iEnd) return null; ???
- 递归函数
dfs(iStart, iEnd, pEnd)
- 重复子问题:
- 以
pE.val
为值创建本次递归的节点
- 在中序数组
[iStart, iEnd]
中,找到与pE.val
相等的分割点 index
- 递归
[iStart, index-1]
,[index+1, iEnd]
,作为新的[iStart, iEnd]
- 递归左右区间,来连接当前节点的左右子节点(
重点:找到两种递归对应的 pE
) - 返回当前节点
- 以
- 递归出口
-
边界条件:
- 当
inStart > inEnd
时返回null
后序数组的最后一个元素,总是当前子树的根节点
- 当
-
复杂度分析:
- 时间复杂度:
O(n) - 每个节点处理一次
- 空间复杂度:
O(n) - 递归栈空间
- 时间复杂度:
与先序+中序构造的区别
遍历组合 | 根节点位置 | 左右子树划分依据 |
---|---|---|
前序+中序 | 前序第一个 | 中序根节点位置 |
后序+中序 | 后序最后一个 | 中序根节点位置 |
注意事项
使用索引避免数组拷贝
,提升性能注意区间闭合范围
(本文采用双闭区间)- 计算左子树大小时要
减去起始位置
class Solution {
private int[] inorder;
private int[] postorder;
private int len;
private TreeNode root;
public TreeNode buildTree(int[] inorder1, int[] postorder1) {
inorder = inorder1;
postorder = postorder1;
len = inorder.length;
return dfs(0, len - 1 , len - 1);
}
public TreeNode dfs(int iStart, int iEnd, int pEnd){
// err: 设置返回值为 TreeNode, 通过递归来来连接节点
if(iStart > iEnd){
// err: iEnd - iStart == 0 不能作为递归出口, 否则会越界???
return null;
}
int index = 0; // index 用于找中序数组分割点
int rootVal = postorder[pEnd];
// err: 节点要定义在循环外面, 方便后面递归, 连接左右子节点
TreeNode root = new TreeNode(rootVal);
for(int i = iStart; i <= iEnd; i++){
// err: [iStart, iEnd) 会跳过 iEnd
if(inorder[i] == rootVal){
// err: 刚开始传的是 len-1, 所以不是[pEnd -1]
index = i;
break;
}
}
// err: 递归左右子树时, 区间错误
// inorder [iStart, index-1] [index+1, iEnd]
// pEnd = {pEnd - (iEnd - index) - 1}
// 先在中序遍历数组找右区间长度 iLen, 再利用 pEnd 和 iLen在后序数组中找左区间的最后一个元素
root.left = dfs(iStart, index-1 , pEnd - (iEnd - index) - 1);
root.right = dfs(index+1, iEnd, pEnd-1);
return root;
}
}
从前序与中序遍历序列构造二叉树
题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
}
}
题目解析
class Solution {
int[] preorder;
int[] inorder;
int len;
public TreeNode buildTree(int[] preorder1, int[] inorder1) {
preorder = preorder1;
inorder = inorder1;
len = preorder.length;
return dfs(0, len - 1, 0);
}
private TreeNode dfs(int iStart, int iEnd, int pIndex) {
if (iStart > iEnd) {
return null;
}
int val = preorder[pIndex];
TreeNode root = new TreeNode(val);
int index = 0;
for (int i = iStart; i <= iEnd; i++) {
if (inorder[i] == val) {
index = i;
break;
}
}
root.left = dfs(iStart, index - 1, pIndex + 1);
root.right = dfs(index + 1, iEnd, pIndex + index - iStart + 1);
return root;
}
}
import java.util.*;
class Solution {
private TreeNode traversal(int[] inorder, int inorderBegin, int inorderEnd,
int[] preorder, int preorderBegin, int preorderEnd) {
if (preorderBegin == preorderEnd) return null;
int rootValue = preorder[preorderBegin];
TreeNode root = new TreeNode(rootValue);
if (preorderEnd - preorderBegin == 1) return root;
int delimiterIndex;
for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
// 切割前序数组
int leftPreorderBegin = preorderBegin + 1;
int leftPreorderEnd = preorderBegin + 1 + (delimiterIndex - inorderBegin);
int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
int rightPreorderEnd = preorderEnd;
root.left = traversal(inorder, leftInorderBegin, leftInorderEnd,
preorder, leftPreorderBegin, leftPreorderEnd);
root.right = traversal(inorder, rightInorderBegin, rightInorderEnd,
preorder, rightPreorderBegin, rightPreorderEnd);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (inorder.length == 0 || preorder.length == 0) return null;
return traversal(inorder, 0, inorder.length, preorder, 0, preorder.length);
}
}
思考题:前序和后序是否可以唯一确定一棵二叉树?
- 问题:
前序和后序是否可以唯一确定一棵二叉树?
- 答案:不可以。
前序和中序
可以唯一确定一棵二叉树
,因为中序遍历可以明确左右子树的分界
。后序和中序
也可以唯一确定一棵二叉树
,原因同上。- 但
前序和后序
无法唯一确定一棵二叉树,因为没有中序遍历,无法明确左右子树的分界
。
- 例子:
tree1
的前序遍历是[1, 2, 3]
,后序遍历是[3, 2, 1]
。tree2
的前序遍历是[1, 2, 3]
,后序遍历是[3, 2, 1]
。- 这两棵树的前序和后序完全相同,但它们是不同的树。
- 问题描述
- 根据
前序遍历和中序遍历
的数组重建二叉树。 前序遍历
的第一个元素是根节点
,中序遍历
中根节点的位置可以划分左右子树
。
- 根据
- 递归逻辑
- 使用
前序遍历
的第一个元素确定根节点。 - 在
中序遍历
中找到根节点的位置
,从而划分左右子树
。 - 递归地对左右子树进行相同的操作。
- 使用
- 代码实现
- 使用递归函数
traversal
,传入当前子树的前序和中序遍历的范围。 - 通过中序遍历的分界点,确定左右子树的范围。
- 递归构建左子树和右子树。
- 使用递归函数
- 思考题总结
前序和中序
、后序和中序
可以唯一确定一棵二叉树
,因为中序遍历
提供了左右子树的分界信息
。前序和后序
无法唯一确定一棵二叉树,因为缺少左右子树的分界信息。
- 调试建议
- 在实现复杂递归逻辑时,建议添加日志打印,观察递归过程是否符合预期。
- 避免仅通过脑动模拟,实际编码并调试是掌握这类问题的关键。