LeetCode 第 236 题「二叉树的最近公共祖先」
字数 1747 2025-10-25 17:23:54
好的,我们这次来详细讲解 LeetCode 第 236 题「二叉树的最近公共祖先」。
这是一道非常经典且高频的二叉树题目,我会从问题描述开始,一步步带你分析思路,并给出清晰的解法。
1. 题目描述
题目链接:LeetCode 236. Lowest Common Ancestor of a Binary Tree
难度:中等
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先(LCA)。
最近公共祖先的定义:
对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是 3。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是 5,因为根据定义,一个节点可以是自身的祖先。
注意:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
2. 题意理解与关键点
公共祖先:节点 x 是 p 和 q 的公共祖先,意味着从根到 p 的路径和根到 q 的路径都会经过 x。
最近:在公共祖先中深度最大的那个。
关键点:
- 如果 p 和 q 分别在某个节点 x 的左右子树中,那么 x 就是它们的最近公共祖先。
- 如果 p 是 q 的祖先,那么 p 就是它们的最近公共祖先(反过来也一样)。
- 题目保证 p 和 q 一定在树中,所以不需要考虑不存在的情况。
3. 思路分析
3.1 暴力思路(不推荐)
我们可以先分别记录从根到 p 和根到 q 的路径,然后找两条路径最后一个相同的节点。
这种方法需要存储路径,空间复杂度 O(h),时间复杂度 O(n),但实现略繁琐。
3.2 递归分治思路(最优)
我们可以用深度优先遍历(后序遍历)的思想:
递归定义:
函数 lowestCommonAncestor(root, p, q) 返回以 root 为根的树中 p 和 q 的最近公共祖先。
递归过程:
- 如果 root 为空,返回 null。
- 如果 root 等于 p 或 q,返回 root(因为 root 是 p 或 q 的祖先)。
- 递归在左子树中找 p 和 q 的公共祖先,得到 left。
- 递归在右子树中找 p 和 q 的公共祖先,得到 right。
- 根据 left 和 right 的情况判断:
- 如果 left 和 right 都不为空,说明 p 和 q 分别在 root 的左右子树中,那么 root 就是它们的最近公共祖先。
- 如果 left 不为空,right 为空,说明 p 和 q 都在左子树,返回 left。
- 如果 right 不为空,left 为空,说明 p 和 q 都在右子树,返回 right。
- 如果都为空,返回 null。
4. 递归解法详细步骤
我们一步步模拟示例 2 来理解。
示例 2:
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
树结构:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
p=5, q=4。
递归过程:
- 从 root=3 开始:
- 3 不是 p 或 q,递归左子树 root=5。
- 在节点 5:
- 5 就是 p,所以返回 5(表示在子树 5 中找到了 p 或 q)。
- 回到节点 3 的左子树调用结束,得到 left=5。
- 递归右子树 root=1:
- 1 不是 p 或 q,递归左子树 root=0:
- 0 不是 p 或 q,递归左子树 null → 返回 null,递归右子树 null → 返回 null,所以 0 返回 null。
- 递归右子树 root=8:
- 同理返回 null。
- 所以 1 的左右都是 null,返回 null。
- 1 不是 p 或 q,递归左子树 root=0:
- 回到节点 3:left=5, right=null,所以返回 left=5。
结果:5 是 p 和 q 的最近公共祖先。
5. 代码实现(Java)
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 如果 root 为空,返回 null
if (root == null) return null;
// 如果 root 是 p 或 q,返回 root
if (root == p || root == q) return root;
// 递归在左子树和右子树中查找
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果左右都不为空,说明 p 和 q 在 root 的两侧,root 是 LCA
if (left != null && right != null) return root;
// 否则返回非空的那一边
return left != null ? left : right;
}
}
6. 复杂度分析
- 时间复杂度:O(n),每个节点最多访问一次。
- 空间复杂度:O(h),h 是树的高度,递归栈的深度。
7. 总结
这道题的核心是后序遍历和分治思想:
- 从底向上传递信息(是否找到 p 或 q)。
- 利用“最近”的特性,当 p 和 q 分布在某个节点的左右两侧时,该节点就是答案。
- 如果只在某一侧,则答案就在那一侧。
这个解法简洁高效,是二叉树 LCA 问题的标准解法。