力扣刷题#32-0236-二叉树的最近公共祖先

力扣刷题#32-0236-二叉树的最近公共祖先

题目

给定一个二叉树,找到该树中两个指定节点的最近公共祖先(LCA)。

百度百科定义:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和 1 的最近公共祖先是 3
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和 4 的最近公共祖先是 5(5 是 4 的祖先)

我的思路之旅:从"想法乱"到"顿悟"

一开始:想法很乱

看到这题,我冒出很多念头:DFS?回溯?保留根状态去算距离?——全是错的切入点。

我犯的核心错误是从上往下想(记录路径、算距离)。但这个问题的最佳视角是从下往上确认

生活化类比

在家族谱里找两个人的"最近共同祖先",不是从老祖宗往下数,而是让每个人往上问"这两个孩子在你这支里吗"——第一个"两个孩子分居左右两支"的人就是答案。


顿悟 1:递归的返回阶段天然是"自下而上"

我最初的困惑:代码只能 root->left 往下走,怎么从叶子往上找?

关键:递归有"往下钻"和"返回"两个阶段。当 dfs(root) 的左右孩子调用都返回后,root 就掌握了整棵子树里有没有 p、有没有 q——因为递归在返回时把信息带回来了。

dfs(3)
  → dfs(5) → ... → 返回   ← 回到 5 时,整棵子树情况已知
  → dfs(1) → ... → 返回   ← 回到 1 时,整棵子树情况已知
  → 回到 3,左右两边都汇报了

不需要真的"从叶子往上爬"——递归的返回阶段就是自下而上。


顿悟 2:返回值为什么不能是 bool?

我一开始想用 bool 表示"子树里找到了 p/q",在 true && true 时返回祖先。但发现不行:

递归一层层往上走。假设节点 5 找到了 LCA,它要把答案传给父节点 3,3 再传上去。如果返回 bool,5 只能说"我这边有情况",但说不出"答案节点是 5"。父节点只知道 true,不知道答案是谁。

结论:返回值必须是 TreeNode*,让"值得上报的节点"一路传回顶层。


顿悟 3:祖先不会被误报(这题最精妙的地方)

我担心 if (left && right) return root; 会不会导致祖先的父节点也被错误上报。

不会,因为一个数学性质:LCA 之上,永远只有一边有货。

当 LCA(记 L)被找到并返回给父节点 P 时,p 和 q 都在 L 的子树里(L 是它们的公共祖先)。所以从 P 的视角看,两个目标在同一侧,另一侧是空的:

P 的 left = L 的整棵子树(p、q 都在这边)→ 有货
P 的 right = 空                                     → 没货

if (left && right)? 不触发(右边没有)
if (left)? 触发 → 返回的还是 L

P 永远不可能返回自己if (left && right) 只在 p、q 分居左右两侧的节点触发一次——那个节点就是 LCA,上面的所有祖先只会把它当"找到的节点"继续上报。


我的 AC 代码

class Solution {
public:
    TreeNode* dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr) return nullptr;
        if (root == p || root == q) return root;   // 自身是目标之一
        TreeNode* left = dfs(root->left, p, q);
        TreeNode* right = dfs(root->right, p, q);
        if (left && right) return root;    // 左右各找到 → root 是 LCA
        if (left) return left;             // 只有左边有 → 上报
        if (right) return right;           // 只有右边有 → 上报
        return nullptr;                    // 都没有 → 报空
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        return dfs(root, p, q);
    }
};

执行:69ms / 43MB(击败 48% / 15%)——数据正常,O(n) 时间是理论最优,排名波动受服务器和测试用例影响,与代码质量无关。


代码逐段解析

第 3 行:空节点

if (root == nullptr) return nullptr;

没有节点,啥也没找到。

第 4 行:自身是目标

if (root == p || root == q) return root;

当前节点就是 p 或 q——返回自身。这正好处理了"p 是 q 的祖先"的情况:p 返回自己,而 p 的子树里还能找到 q,最终 p 成为两边汇合点。

第 5-6 行:递归收集左右

TreeNode* left = dfs(root->left, p, q);
TreeNode* right = dfs(root->right, p, q);

注意:必须接住返回值(第一版代码我就是漏了存 left/right 卡住了)。

第 7-10 行:三分支判断

条件 含义 返回
left && right 左右各找到一个 → p、q 在此分开 root(LCA)
left 只有左边有货 left(继续上报)
right 只有右边有货 right(继续上报)
都空 这棵子树啥也没有 nullptr

完整走一遍

        3
       / \
      5   1
     / \
    6   2
       /
      7
p=6, q=7 → LCA = 5
节点 left right 触发分支 返回
7 null null return nullptr null
6 null null 自身是p 6
2 7(有) null if(left) 7
5 6(有) 7(有) if(left&&right) 5 = LCA
3 5(有) null if(left) 5(继续上报)

节点 3 虽然两边都看过,但只有左边有货 → 返回的还是 5,不会误报成 3。答案精确回到 LCA。


复杂度分析

维度 说明
时间复杂度 O(n) 每个节点最多访问一次
空间复杂度 O(h) 递归栈深度 = 树高

关键点总结

关键点 说明
切入点 从下往上确认,不是从上往下记录
返回值 必须是 TreeNode*(bool 传不了答案节点)
三种上报 找到p/q → 返回自身;两边有 → 返回root;单边有 → 上报那边
祖先不误报 LCA 之上永远只有一边有货 → 只会继续上报 LCA
递归返回阶段 天然自下而上,信息在返回时带回

本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

posted @ 2026-08-12 12:47  STA_running  阅读(1)  评论(0)    收藏  举报