236. 二叉树的最近公共祖先
236. 二叉树的最近公共祖先
题目链接(中等)
题目描述
给定一个二叉树,找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:「对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。」
数据范围:
- 树中节点数目在范围
[2, 10^5]内 -10^9 <= Node.val <= 10^9- 所有
Node.val互不相同 p != qp和q均存在于给定的二叉树中
示例
示例 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。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入: root = [1,2], p = 1, q = 2
输出: 1
核心思路
最近公共祖先(LCA):节点 x 是 p、q 的公共祖先,且深度尽可能大。
分类讨论:
- 如果
p、q分别位于x的左右子树,那么x就是 LCA; - 如果
x本身就是p或q,而另一个节点在x的子树中,那么x也是 LCA; - 如果
p、q都在同一侧子树,则 LCA 在那棵子树里,需要继续递归。
三种解法:
- 递归(返回节点):递归函数直接返回 LCA 节点,最优雅;
- 递归(返回布尔值):用辅助变量记录 LCA,思路像「找两侧各一个」;
- 存储父节点:用哈希表记录每个节点的父节点,从
p往上标记祖先,再从q往上找。
方法一:递归(直接返回 LCA 节点)✅ 推荐
思路及解法
定义递归函数 lowestCommonAncestor(root, p, q),返回「以 root 为根的子树中,p 和 q 的 LCA」。
递归终止与返回:
- 若
root为空:返回None; - 若
root == p或root == q:直接返回root(找到了其中一个,它自己可能就是 LCA,或者它的祖先中会有另一侧凑齐)。
递归逻辑:
- 递归左子树得到
left; - 递归右子树得到
right; - 判断:
left和right都非空:说明左右子树各找到一个目标,root就是 LCA,返回root;- 只有
left非空:说明两个目标都在左子树里,返回left; - 只有
right非空:同理返回right。
直觉理解:
- 如果
p和q在root的两侧,那么left和right都会返回非空,root就是 LCA; - 如果它们都在同一侧,那一侧的递归结果就是 LCA,另一侧返回
None,直接把这个结果「向上传递」。
代码极其简洁,只有几行,是面试中最常写的版本。
代码
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root is p or root is q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left or right复杂度分析
- 时间复杂度:$O(n)$,每个节点访问一次。
- 空间复杂度:$O(n)$,递归栈深度最坏为 $n$(链状树)。
方法二:递归(返回布尔值 + 辅助变量)
思路及解法
定义 dfs(root) 返回「以 root 为根的子树中是否包含 p 或 q」(布尔值)。
递归过程:
- 如果
root为空,返回False; - 递归左子树得到
lson; - 递归右子树得到
rson; - 判断
root是否是 LCA:lson && rson:左右子树各包含一个目标节点,root就是 LCA;root == p || root == q:root本身就是其中一个目标,另一个在它子树中,root也是 LCA;
- 返回
lson || rson || root is p || root is q。
为什么找到 LCA 后,不会被更上层的祖先「抢答」?
因为找到 LCA 后,dfs(LCA) 只返回 True。更上层的祖先只会知道「子树里有目标」,但如果另一侧没有目标,就不满足 lson && rson,不会被误判为 LCA。
代码
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
self.ans = None
def dfs(node: 'TreeNode') -> bool:
if not node:
return False
lson = dfs(node.left)
rson = dfs(node.right)
if (lson and rson) or node is p or node is q:
self.ans = node
return lson or rson or node is p or node is q
dfs(root)
return self.ans复杂度分析
- 时间复杂度:$O(n)$。
- 空间复杂度:$O(n)$,递归栈深度。
方法三:存储父节点(哈希表 + 向上跳)
思路及解法
核心思路:如果我们知道每个节点的父节点,就可以从 p 向上走到根,再从 q 向上走,找到第一个「两条路径的交点」,那就是 LCA。这和「两个链表找交点」是同一个套路。
流程:
- DFS 建表:遍历整棵树,用哈希表
fa记录每个节点的父节点; - 标记 p 的祖先:从
p出发,一路往父节点走,把经过的节点记入visited; - 从 q 往上找:从
q出发,一路往父节点走,遇到的第一个在visited中的节点就是 LCA。
为什么是 LCA?
visited里存的是p的所有祖先;q从自己往上走,遇到的第一个p的祖先,就是「同时是p和q的祖先」中离 q 最近的,即深度最大的公共祖先。
代码
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
fa = {root: None}
# DFS 记录每个节点的父节点
def dfs(node: 'TreeNode') -> None:
if node.left:
fa[node.left] = node
dfs(node.left)
if node.right:
fa[node.right] = node
dfs(node.right)
dfs(root)
# 从 p 往上标记所有祖先
visited = set()
while p:
visited.add(p)
p = fa[p]
# 从 q 往上找第一个已访问的祖先
while q:
if q in visited:
return q
q = fa[q]
return None复杂度分析
- 时间复杂度:$O(n)$,DFS 遍历所有节点,之后
p、q往上跳最多 $n$ 步。 - 空间复杂度:$O(n)$,哈希表
fa、集合visited、递归栈各 $O(n)$。
关键细节
1. 方法一中「找到了就直接返回」
if not root or root is p or root is q:
return root- 如果
root == p,那么无论q在root的子树中还是在子树外,root都是 LCA 的候选(要么就是 LCA 本身,要么它的祖先里会凑齐q); - 直接把
root返回给上一层,让上层来决定最终答案。
2. 方法一中 left and right 的判断
- 如果
left和right都非空,说明左右子树各找到了一个目标,root是唯一的 LCA; - 如果只有一侧非空,说明两个目标都在那一侧,
root不是 LCA,把那一侧的结果向上传。
3. 方法二中为什么要用 self.ans
布尔返回值无法直接传递 LCA,需要用外部变量记录。因为 dfs 只返回「子树里有没有目标」,所以发现 LCA 时需要先存起来,再继续向上返回 True。
4. 用 is 还是 == 比较节点
题目保证所有 Node.val 互不相同,理论上 == 也可以。但 Python 的 TreeNode 默认没有实现 __eq__,所以 == 实际比较的是对象身份,和 is 等价。
从语义上讲,node is p 更明确,表示「是同一个节点对象」,推荐使用。
5. 三种方法的共同点
- 都是「自底向上」的思路:先递归到底部,再往回判断;
- 都利用了「
p和q分居两侧时,当前节点即 LCA」这个关键性质; - 时间复杂度都是 $O(n)$,空间都是 $O(n)$。
总结
- LCA 的核心逻辑:
p、q分布在x的左右两侧,或x本身就是其中一个,则x是 LCA; - 方法一(推荐):
- 递归返回 LCA 节点;
root是p或q时直接返回root;- 左右都非空则
root是 LCA; - 否则返回非空的那一侧;
- 方法二:返回布尔值,用
self.ans记录 LCA; - 方法三:哈希表记父节点,从
p往上标记,从q往上找交点; - 三种方法时间都是 $O(n)$,空间都是 $O(n)$。
相关题目
- LC 235. 二叉搜索树的最近公共祖先(利用 BST 性质)
- LC 1644. 二叉树的最近公共祖先 II(p、q 可能不存在)
- LC 1650. 二叉树的最近公共祖先 III(带父指针)
- LC 1676. 二叉树的最近公共祖先 IV(多个节点)
- LC 1123. 最深叶节点的最近公共祖先(变形)