问题描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
1 | 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 |
示例 2:
1 | 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 |
示例 3:
1 | 输入:root = [1,2], p = 1, q = 2 |
提示:
- 树中节点数目在范围 [2, 10^5] 内。
- -10^9 <= Node.val <= 10^9
- 所有 Node.val 互不相同。
- p != q
- p 和 q 均存在于给定的二叉树中。
解题思路
这个问题看起来很复杂,但通过递归方法可以优雅地解决。关键洞见是:如果我们从根节点开始,对于任意节点:
- 如果当前节点就是 p 或 q,那么这个节点就是它们的最近公共祖先(可能一个是另一个的后代)
- 如果当前节点不是 p 或 q,我们需要在左右子树中寻找 p 和 q:
- 如果 p 和 q 分别在当前节点的左右子树中,那么当前节点就是最近公共祖先
- 如果 p 和 q 都在左子树,那么最近公共祖先在左子树中
- 如果 p 和 q 都在右子树,那么最近公共祖先在右子树中
这个问题可以通过几种方法解决:
方法一:递归 - 后序遍历
递归是解决这个问题最直观的方法。我们可以通过后序遍历(左、右、根)的方式,自底向上地寻找最近公共祖先。
实现思路:
-
基本情况:
- 如果当前节点为空,返回 nil
- 如果当前节点等于 p 或 q,返回当前节点
-
递归调用:
- 在左子树中递归寻找 p 和 q
- 在右子树中递归寻找 p 和 q
-
合并结果:
- 如果左子树和右子树的递归调用都返回非空结果,说明 p 和 q 分别在当前节点的左右子树中,当前节点就是最近公共祖先
- 如果只有一个子树返回非空结果,则该结果就是最近公共祖先
- 如果两个子树都返回空,则返回空
方法二:使用父节点映射
我们也可以通过建立每个节点到其父节点的映射来解决这个问题:
- 从根节点开始DFS,创建每个节点到其父节点的映射
- 从 p 开始,向上遍历至根,记录经过的所有节点
- 从 q 开始,向上遍历至根,找到第一个在 p 的路径中出现的节点,该节点即为最近公共祖先
方法三:路径比较法
- 找出从根节点到 p 的路径
- 找出从根节点到 q 的路径
- 比较这两条路径,找到最后一个相同的节点,即为最近公共祖先
代码实现
方法一:递归实现
1 | /** |
方法二:父节点映射实现
1 | func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { |
方法三:路径比较法
1 | func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { |
方法比较
方面 | 方法一:递归 | 方法二:父节点映射 | 方法三:路径比较法 |
---|---|---|---|
时间复杂度 | O(n) | O(n) | O(n) |
空间复杂度 | O(h),h为树高 | O(n) | O(n) |
优点 | 代码简洁,空间效率高 | 思路直观 | 容易理解 |
缺点 | 递归可能导致栈溢出 | 需要额外空间存储父节点映射 | 需要额外空间存储路径 |
推荐度 | ★★★★★ | ★★★☆☆ | ★★★☆☆ |
复杂度分析
方法一:递归
- 时间复杂度: O(n),其中 n 是树中的节点数。在最坏情况下,我们需要访问树中的所有节点。
- 空间复杂度: O(h),其中 h 是树的高度。递归调用的栈深度与树的高度成正比。在最坏情况下(树是一条链),空间复杂度为 O(n)。
方法二:父节点映射
- 时间复杂度: O(n),我们需要遍历整棵树来构建父节点映射,然后在最坏情况下需要遍历从 p 和 q 到根的所有节点。
- 空间复杂度: O(n),需要额外空间存储父节点映射和访问集合。
方法三:路径比较法
- 时间复杂度: O(n),需要遍历树来找到从根到 p 和 q 的路径。
- 空间复杂度: O(n),需要存储从根到 p 和 q 的路径。
关键收获
-
递归思想在树问题中的应用:树的问题通常可以通过递归优雅地解决,尤其是自底向上的后序遍历。
-
最近公共祖先的特性:
- 如果两个节点在不同的子树中,它们的LCA就是子树的根节点
- 如果一个节点是另一个节点的祖先,那么这个祖先节点就是LCA
-
路径表示法:从根到节点的路径可以唯一确定一个节点,比较两条路径可以找到分叉点
-
空返回值的处理:在递归中处理空返回值是很重要的,确保代码的健壮性
这道题是树的经典问题,它不仅考察了树的遍历技巧,还考察了对递归的理解和应用。方法一(递归)是最简洁和高效的解法,也是面试中推荐使用的方法。