【LeetCode 热题 100】236. 二叉树的最近公共祖先——DFS
Problem: 236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(H)
整体思路
这段代码旨在解决一个经典的树形问题:二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree)。问题要求在一棵普通的二叉树(非二叉搜索树)中,找到两个给定节点 p
和 q
的最近公共祖先(LCA)。
该算法采用了一种非常优雅且高效的 深度优先搜索 (DFS) 的后序遍历 策略。其核心思想是自底向上地传递信息,通过递归函数的返回值来判断LCA的位置。
-
递归函数的定义与含义
lowestCommonAncestor(root, p, q)
这个递归函数的返回值具有特定的含义:- 如果在以
root
为根的子树中能找到p
或q
(或两者都能找到,但一个是另一个的祖先),函数就返回那个找到的节点 (p
或q
)。 - 如果在
root
的左右子树中分别找到了p
和q
,函数就返回root
本身,因为root
就是它们的LCA。 - 如果在以
root
为根的子树中一个都找不到,函数就返回null
。
- 如果在以
-
递归的终止条件(Base Case)
if (root == null || root == p || root == q)
: 这是递归的出口。root == null
: 如果当前节点为空,说明这条路径走到底了,什么也没找到,返回null
。root == p || root == q
: 如果当前节点就是p
或q
中的一个,根据我们对函数返回值的定义,说明我们“找到”了目标,直接返回当前节点root
。
-
递归的分解与合并(后序遍历逻辑)
- 分解:
TreeNode left = lowestCommonAncestor(root.left, p, q);
:递归地在左子树中查找p
或q
。TreeNode right = lowestCommonAncestor(root.right, p, q);
:递归地在右子树中查找p
或q
。
- 合并(这是后序遍历的核心,在处理完左右子树后进行判断):
if (left != null && right != null)
: 这是最关键的判断。如果left
和right
都不是null
,这意味着p
和q
分别位于当前root
节点的左右子树中。根据LCA的定义,当前root
就是它们的最近公共祖先。因此,返回root
。return left != null ? left : right;
: 如果上面那个if
不成立,说明p
和q
不在root
的两侧。这种情况分为两种:- 只有一个子树找到了目标:例如,
left
不是null
而right
是null
。这说明p
和q
都在左子树中,并且left
返回的是它们中更“高”的那个(即它们的LCA)。我们只需将这个结果继续向上传递即可。所以返回left
。同理,如果只有右子树有结果,就返回right
。 - 两个子树都没找到:
left
和right
都是null
,那么就返回null
。 - 这个三元表达式简洁地处理了这两种情况。
- 只有一个子树找到了目标:例如,
- 分解:
通过这种自底向上的信息传递,算法能够准确地在第一次相遇 p
和 q
的分叉点处确定LCA。
完整代码
// Definition for a binary tree node.
// public class TreeNode {
// int val;
// TreeNode left;
// TreeNode right;
// TreeNode(int x) { val = x; }
// }class Solution {/*** 查找二叉树中两个给定节点 p 和 q 的最近公共祖先 (LCA)。* @param root 树的根节点* @param p 目标节点一* @param q 目标节点二* @return p 和 q 的最近公共祖先节点*/public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 步骤 1: 递归的终止条件 (Base Case)// 如果当前节点为空,或者当前节点就是 p 或 q,则直接返回当前节点。// - root == null: 搜索到了叶子节点的子节点,路径结束,返回 null。// - root == p || root == q: 找到了目标节点之一,返回该节点。if (root == null || root == p || root == q) {return root;}// 步骤 2: 递归分解 (后序遍历的前两步)// 在左子树中递归查找 p 或 q。TreeNode left = lowestCommonAncestor(root.left, p, q);// 在右子树中递归查找 p 或 q。TreeNode right = lowestCommonAncestor(root.right, p, q);// 步骤 3: 结果合并 (后序遍历的最后一步)// 基于左右子树的返回结果,判断当前 root 的角色。// Case 1: p 和 q 分别位于 root 的左右子树中。// 此时,left 返回 p (或其祖先),right 返回 q (或其祖先)。// 那么当前 root 就是它们的最近公共祖先。if (left != null && right != null) {return root;}// Case 2: p 和 q 都在同一侧子树中,或者其中一个是另一个的祖先。// - 如果 left 非空,right 为空,说明 p 和 q 都在左子树,left 就是它们在左子树中的LCA,继续向上传递。// - 如果 right 非空,left 为空,说明 p 和 q 都在右子树,right 就是它们在右子树中的LCA,继续向上传递。// - 如果都为空,说明此子树中没有 p 或 q,返回 null。// 这个三元表达式优雅地处理了以上所有情况。return left != null ? left : right;}
}
时空复杂度
时间复杂度:O(N)
- 遍历:该算法本质上是一次深度优先搜索(DFS)。在最坏的情况下,它需要访问树中的每一个节点一次。
- 节点操作:对于每个节点,执行的操作(比较、递归调用)都是常数时间的。
综合分析:
算法的总时间复杂度与树中的节点数 N
成正比。因此,时间复杂度为 O(N)。
空间复杂度:O(H)
- 主要存储开销:算法的空间开销主要来自于递归调用栈 (recursion call stack)。
- 空间大小:递归栈的深度取决于树的高度
H
。- 最坏情况:如果树是一个极度不平衡的链状结构,树的高度
H
会约等于节点数N
。此时,空间复杂度为 O(N)。 - 最好情况(平衡二叉树):如果树是完全平衡的,树的高度
H
约等于log N
。此时,空间复杂度为 O(log N)。
- 最坏情况:如果树是一个极度不平衡的链状结构,树的高度
综合分析:
算法所需的额外空间由递归栈的深度决定,因此空间复杂度为 O(H),其中 H
是二叉树的高度。
参考灵神