二叉树算法下篇:递归核心技巧与高频面试题详解

文章目录

    • 前言
    • [一、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% 的套路。刷完它们,再碰到二叉树题基本就是套娃。


相关推荐
my_power5201 小时前
大模型概述学习笔记
笔记·学习
水龙吟啸1 小时前
华为2026.6.24机考选择题+编程题【速刷敲黑板】
人工智能·深度学习·算法·机器学习·华为
Liquad Li1 小时前
Salesforce高级开发面试题
java·网络·面试·crm·salesforce
Eloudy1 小时前
全文 - Evolution Strategies as a Scalable Alternative to Reinforcement Learning
人工智能·算法
Zhou1411362 小时前
Java常见面试题7
java·开发语言
YUS云生2 小时前
Python学习笔记·第28天:Git入门——版本控制与基础操作
笔记·python·学习
Yang_jie_032 小时前
笔记:数据结构(链表)
数据结构·笔记·链表
xiaoyuchidayuma2 小时前
【# 电压极限圆、电流极限圆、MTPA曲线、最大功率曲线的关系】
笔记·学习
net3m332 小时前
可以加大adc滑动滤波的间隔时间,比如定时每20ms采集一次滤波输入,且用滑动窗口均值滤波
算法