文章目录
-
- 前言
- [一、LC105 从前序与中序遍历构造二叉树](#一、LC105 从前序与中序遍历构造二叉树)
-
- [1.1 题意](#1.1 题意)
- [1.2 最优解:HashMap + 递归](#1.2 最优解:HashMap + 递归)
- [二、LC437 路径总和 III](#二、LC437 路径总和 III)
-
- [2.1 题意](#2.1 题意)
- [2.2 核心思想:前缀和 + 回溯](#2.2 核心思想:前缀和 + 回溯)
- [2.3 代码](#2.3 代码)
- [2.4 最容易忽略的坑](#2.4 最容易忽略的坑)
- [三、LC236 二叉树的最近公共祖先](#三、LC236 二叉树的最近公共祖先)
-
- [3.1 题意](#3.1 题意)
- [3.2 最优递归:4 行搞定](#3.2 最优递归:4 行搞定)
- [3.3 真实翻车现场](#3.3 真实翻车现场)
- [四、LC124 二叉树中的最大路径和](#四、LC124 二叉树中的最大路径和)
-
- [4.1 题意](#4.1 题意)
- [4.2 核心拆解](#4.2 核心拆解)
- [4.3 最优代码](#4.3 最优代码)
- [4.4 翻车实录:多条件判断把自己绕晕](#4.4 翻车实录:多条件判断把自己绕晕)
- 五、递归通用心法
-
- [5.1 返回值 vs 全局变量](#5.1 返回值 vs 全局变量)
- [5.2 能砍就砍:Math.max(0, x)](#5.2 能砍就砍:Math.max(0, x))
- [5.3 回溯 = 进时加,出时减](#5.3 回溯 = 进时加,出时减)
- 总结
前言
这篇文章是二叉树算法系列的下篇,带你搞定 4 道高频递归题:构造二叉树(105) 、路径前缀和(437) 、最近公共祖先(236) 、最大路径和(124)。每道题只讲最优雅的解法,附带真实翻车案例,读完你能少踩 80% 的坑。
上篇讲了二叉树遍历框架和基础题,没看过的建议先过一遍:《二叉树算法上篇》
一、LC105 从前序与中序遍历构造二叉树
1.1 题意
给前序 [根|左|右] 和中序 [左|根|右],重建二叉树。
前序: [3, 9, 20, 15, 7]
中序: [9, 3, 15, 20, 7]
结果:
3
/ \
9 20
/ \
15 7
1.2 最优解:HashMap + 递归
前序第一个就是根,中序里根左边是左子树,右边是右子树。用 HashMap 存中序下标,O(1) 定位根。
java
class Solution {
private Map<Integer, Integer> map = new HashMap<>();
private int[] preorder;
private int preIdx = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return build(0, inorder.length - 1);
}
private TreeNode build(int inL, int inR) {
if (inL > inR) return null;
int rootVal = preorder[preIdx++];
TreeNode root = new TreeNode(rootVal);
int mid = map.get(rootVal);
root.left = build(inL, mid - 1);
root.right = build(mid + 1, inR);
return root;
}
}
关键:先递归左子树,再右子树。 顺序不能反,因为 preIdx 按前序顺序推进。
- 时间 O(n) ,空间 O(n)
- 不用 HashMap 每次中序里线性查找 → O(n²),过不了大数据
二、LC437 路径总和 III
2.1 题意
求二叉树中路径和等于 target 的路径条数。路径不要求从根出发,也可以不到叶子,但必须是自上而下的连续节点。
2.2 核心思想:前缀和 + 回溯
和 LeetCode 560"和为 K 的子数组"思路一致,只是把数组换成了从根到当前节点的这条路径。维护一个 HashMap 记录路径上出现过的前缀和次数。
如果到当前节点的前缀和 = cur
之前出现过前缀和 = cur - target
那中间那段的和就是 target
2.3 代码
java
class Solution {
public int pathSum(TreeNode root, int targetSum) {
Map<Long, Integer> pre = new HashMap<>();
pre.put(0L, 1); // 处理从根开始就等于 target 的情况
return dfs(root, 0L, targetSum, pre);
}
private int dfs(TreeNode node, long cur, int target, Map<Long, Integer> pre) {
if (node == null) return 0;
cur += node.val;
int res = pre.getOrDefault(cur - target, 0);
pre.merge(cur, 1, Integer::sum);
res += dfs(node.left, cur, target, pre);
res += dfs(node.right, cur, target, pre);
pre.merge(cur, -1, Integer::sum); // 回溯:离开节点必须撤销
return res;
}
}
2.4 最容易忽略的坑
回溯不撤销,结果会错。 HashMap 里的前缀和必须只包含"当前路径"上的节点。走到另一条分支时,之前分支的记录还在就会污染统计。
假如不撤销:
10 → 5 → 3 这条链的前缀和 10, 15, 18 都在 map 里
回到 10 走右边 10 → -3 时,15 还在 map 里
→ 算出来的路径条数偏多
三、LC236 二叉树的最近公共祖先
3.1 题意
找 p 和 q 在二叉树中的最近公共祖先(LCA)。
3.2 最优递归:4 行搞定
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode l = lowestCommonAncestor(root.left, p, q);
TreeNode r = lowestCommonAncestor(root.right, p, q);
if (l != null && r != null) return root; // 分叉点 = LCA
return l != null ? l : r;
}
}
逻辑非常简洁:
- p、q 分别在左右子树 → root 就是 LCA
- 只有一个非空 → 把非空的往上透传
- 遇到 p 或 q 直接返回,子树不再遍历(天然剪枝)
3.3 真实翻车现场
我在写这题时踩过两个坑:
坑 1:用类字段存 ans,多此一举。
java
// 错误示范
TreeNode ans = null;
TreeNode p, q; // 字段赋值开销大,每次对比走 this.p
多了 3 个字段 + 5 个 if 分支,比上面的版本慢了好几 ms。字段访问在树深 50000 时累积开销明显,所有数据走参数传递最干净。
坑 2:遇到 p 或 q 后没剪枝。
java
if (root == p || root == q) {
ans = root; // 记录候选
return root; // 返回了,但子节点照样跑
}
当 p 是 q 的祖先时,ans 被设成 p 但永远不会被"左右各有一个"的逻辑更新。应该遇到 p/q 就直接当 LCA 的组成部分返回,依赖上层判断。
四、LC124 二叉树中的最大路径和
4.1 题意
找二叉树中任意节点到任意节点的路径最大和。Hard 题,难在"路径可以折弯"这个自由度。
-10
/ \
9 20
/ \
15 7
最大路径: 15 → 20 → 7 = 42
4.2 核心拆解
对于节点 root,经过它的最大路径和:
root.val + 左孩子贡献 + 右孩子贡献
其中"孩子贡献" = 从孩子出发,只选一边向下走能获得的最大值。负贡献直接丢弃(不走)。
4.3 最优代码
java
class Solution {
int ans = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null) return 0;
int left = Math.max(0, dfs(root.left)); // 负的就不要
int right = Math.max(0, dfs(root.right));
ans = Math.max(ans, root.val + left + right); // 在 root 折弯
return root.val + Math.max(left, right); // 只选一边,给父节点用
}
}
4.4 翻车实录:多条件判断把自己绕晕
我最开始写的版本长这样:
java
int val = root.val + Math.max(left, right);
if (val < root.val) {
ans = Math.max(root.val, ans);
return root.val;
}
ans = Math.max(root.val + left + right, ans);
return val;
看起来很合理,但漏了一个关键情况: 当 val >= root.val(即至少一边有正贡献),ans 只比较了"在 root 折弯"的路径,val 本身(单边最优)从来没被写进 ans。
5
/ \
4 -3
val = 5 + 4 = 9(正确答案)
ans 只更新了 5+4+(-3)=6
→ 输出 6,实际应该是 9
教训:负贡献直接砍掉(Math.max(0, child)),比你自己分 if-else 可靠得多。 条件分支越多,边界情况越容易漏。
五、递归通用心法
这 4 道题写下来,二叉树递归就三个核心技巧:
5.1 返回值 vs 全局变量
| 场景 | 用什么 |
|---|---|
| 结果是整棵树的某个节点(LCA) | 返回值透传 |
| 结果是全局最优值(最大路径和) | 全局变量 + 返回值协助 |
| 需要跨分支信息(前缀和) | 参数传递 + 回溯 |
局部最优用返回值,全局最优用全局变量。
5.2 能砍就砍:Math.max(0, x)
负贡献直接不要,这是 124 题的核心技巧,也是很多路径题的标准操作。别自己写 if-else 判断,容易漏。
5.3 回溯 = 进时加,出时减
437 的前缀和、回溯法求排列组合,都是同一个模板:
java
map.put(cur, 1); // 进入当前层,加上贡献
dfs(...); // 递归
map.put(cur, 0); // 离开当前层,撤销贡献
忘了撤销 = 数据污染 = 结果错。
总结
LC105 构造:前序定根 + 中序分左右 + HashMap 加速
LC437 路径和:前缀和 + HashMap + 回溯撤销
LC236 公共祖先:自底向上,遇 p/q 返回,分叉即答案
LC124 最大路径和:负贡献丢弃 + ans 记折弯 + 返回值记单边
这 4 道题覆盖了二叉树递归 90% 的套路。刷完它们,再碰到二叉树题基本就是套娃。