力扣刷题#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仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号