一、题目简介
给定一棵二叉树的根节点 root,以及树中的两个节点 p 和 q,找到它们的最近公共祖先(Lowest Common Ancestor)。
最近公共祖先的定义: 对于两个节点 p、q,最近公共祖先是满足「p、q 都是它的子孙节点,且自身深度尽可能大」的节点。
举两个经典例子:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
- 例 1:
p=5,q=1→ 最近公共祖先是3(5 在左子树,1 在右子树,第一次交汇于 3) - 例 2:
p=5,q=4→ 最近公共祖先是5(5 本身就是 4 的祖先)
二、核心卡点:递归函数到底返回什么
这道题最容易钻的牛角尖:递归返回的是 true/false 吗? 不是。
递归函数的返回值,是当前子树中找到的「关键节点」,可以把它理解成子树给父节点传递的「信号」。 信号只有四种可能:
null:这棵子树里既没找到 p,也没找到 qp:这棵子树里找到了 pq:这棵子树里找到了 q- 某个节点:这棵子树里已经找到了 LCA
整个递归的过程,就是自底向上传递信号,直到两个信号第一次交汇的过程。
三、递归终止条件(三种情况直接返回)
if (root == null || root == p || root == q) {
return root;
}
这一行是整道题的基础,拆开理解:
- root == null:走到叶子节点下面了,这条路径什么都没找到,返回 null
- root == p:当前节点就是 p,直接返回 p 这个信号
- root == q:当前节点就是 q,直接返回 q 这个信号
新手高频疑问:找到 p 就直接返回,不用继续往下找 q 了吗? 题目已经保证 p、q 一定存在于树中。当前节点是 p,说明 p 就在这棵子树里;至于 q,要么在 p 的子树中,要么在别的分支,交给上层节点去判断就好。这里只需要把「找到 p」这个信号传上去。
四、核心逻辑:左右递归 + 结果汇总
先分别递归左右子树,拿到两边的信号,再判断当前节点的角色。
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
拿到 left 和 right 两个信号后,一共三种情况:
情况 1:左右都不为空
if (left != null && right != null) {
return root;
}
含义 :左子树找到了一个目标,右子树找到了另一个目标。 说明 p 和 q 分别在当前节点的左右两侧 ,那么当前节点就是它们第一次交汇的位置,也就是最近公共祖先。
情况 2:只有左边不为空
说明两个目标都在左子树里,当前节点肯定不是 LCA,把左子树的结果继续向上传递。
情况 3:只有右边不为空
同理,把右子树的结果继续向上传递。
后两种情况可以合并成一行极简写法:
return left != null ? left : right;
两边都为空时,自然返回 null,逻辑自洽。
五、完整 AC 代码
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 终止条件:空节点、找到p、找到q,直接返回
if (root == null || root == p || root == q) {
return root;
}
// 后序遍历:先递归左右子树
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 左右都找到目标 → 当前节点就是LCA
if (left != null && right != null) {
return root;
}
// 哪边有结果,就把哪边的结果往上传递
return left != null ? left : right;
}
}
六、完整例子一步步走
还是用这棵经典树:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
例子 1:找 p=5,q=1
- 进入节点 3:不是空、不是 p/q,递归左右子树
- 左子树进入节点 5:
root == p,直接返回 5 →left = 5 - 右子树进入节点 1:
root == q,直接返回 1 →right = 1
- 左子树进入节点 5:
- 回到节点 3:left 和 right 都不为空 → 返回当前节点 3 最终答案:3
例子 2:找 p=5,q=4
- 进入节点 3:递归左右子树
- 左子树进入节点 5:
root == p,直接返回 5 →left = 5 - 右子树进入节点 1:往下递归都没找到 4,最终返回 null →
right = null
- 左子树进入节点 5:
- 回到节点 3:只有 left 不为空 → 返回 left,也就是 5 最终答案:5
关键理解:节点 5 直接返回,并没有往下找 4,但结果依然正确。 因为当「5」这个信号传到上层时,上层判断另一边没有信号,说明两个节点都在左子树,而 5 就是更靠近根的那个,也就是 LCA。
七、本质:这是一道后序遍历题
你会发现代码的执行顺序是: 左子树递归 → 右子树递归 → 处理当前节点
这就是标准的后序遍历(Left-Right-Root)。
为什么必须是后序? 因为当前节点要判断自己是不是 LCA,必须先拿到左右子树的完整信息。只有知道左边有没有目标、右边有没有目标,才能确定当前节点的角色。
二叉树题通用规律:只要当前节点的答案依赖左右子树的结果,就用后序递归。
八、新手必踩坑总结
-
终止条件漏掉
root == p || root == q只写root==null,会导致找到目标节点还继续往下递归,逻辑完全错误。 -
以为找到 p 就可以结束整个递归 不行。我们要找的是「交汇点」,必须把信号传到上层去汇总判断,不能提前终止。
-
搞反交汇逻辑 左右都不为空的时候,当前 root 就是 LCA,直接返回;不要继续往上传错节点。
-
额外处理两边都为空的情况 不需要。
return left != null ? left : right两边都空自然返回 null,逻辑已经覆盖。
九、复杂度分析
-
时间复杂度:O (n) 最坏情况下需要遍历树中所有节点一次。
-
空间复杂度:O (h) h 是树的高度,来自递归调用栈。 最坏情况树退化成链表,h=n,空间 O (n); 平衡二叉树 h=logn,空间 O (logn)。
十、面试口述标准回答
我用递归后序遍历来解决这道题。 对于每个节点,先分别在左右子树中寻找 p 和 q。如果当前节点为空,或者当前节点本身就是 p 或 q,就直接返回当前节点。 如果左右子树的递归结果都不为空,说明 p 和 q 分别位于当前节点的左右两侧,那么当前节点就是最近公共祖先。 如果只有一侧的结果不为空,说明目标节点或者已经找到的公共祖先在这一侧,就把这一侧的结果继续向上传递。 时间复杂度 O (n),空间复杂度 O (h),h 是树的高度。
十一、核心总结
这道题的核心可以浓缩成四步:
- 空或等于目标 → 直接返回
- 先查左子树
- 再查右子树
- 两边都有返回自己,哪边有返回哪边
本质就是后序递归 + 信号传递:子树把结果传给父节点,父节点汇总后再往上传递,最终两个信号第一次交汇的地方,就是最近公共祖先。