问题定义与核心挑战
2025年12月9日,LeetCode Hot 100榜单中,"二叉树的最近公共祖先"一题以73.9%的通过率位居中等难度题目前列。这个看似简单的问题,却蕴含着二叉树遍历与递归思想的精髓。给定一棵二叉树的根节点root,以及两个指定节点p和q,我们需要找到这两个节点的最近公共祖先(LCA)。所谓最近公共祖先,是指在二叉树中同时拥有p和q作为后代的最深节点(这里允许节点成为其自身的祖先)。
问题的三个关键特性
公共祖先的定义:节点x是p和q的公共祖先,当且仅当x是p的祖先且x是q的祖先
最近的判定标准:在所有公共祖先中深度最大的那个节点
特殊情况处理:当p是q的祖先时,p就是最近公共祖先;反之亦然
递归解法:从底向上的智慧
算法核心思想
递归解法的精妙之处在于它采用后序遍历的方式,从叶子节点开始向上回溯。这种"自底向上"的策略能让我们在第一次遇到同时包含p和q的节点时,立即确定它就是最近公共祖先。
递归函数的返回值含义
如果当前节点的左子树包含p或q,返回左子树中找到的节点
如果当前节点的右子树包含p或q,返回右子树中找到的节点
如果当前节点本身就是p或q,返回当前节点
如果左右子树分别返回非空节点,说明当前节点就是LCA,返回当前节点
如果左右子树都返回空,返回空
五种语言实现代码
Java实现
class TreeNode {int val;TreeNode left;TreeNode right;TreeNode(int x) { val = x; }}class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 递归终止条件if (root == null || root == p || root == q) {return root;}// 后序遍历TreeNode left = lowestCommonAncestor(root.left, p, q);TreeNode right = lowestCommonAncestor(root.right, p, q);// 左右子树都找到,当前节点是LCAif (left != null && right != null) {return root;}// 只在左子树找到else if (left != null) {return left;}// 只在右子树找到else {return right;}}}
Python实现
class TreeNode:def __init__(self, x):self.val = xself.left = Noneself.right = Noneclass Solution:def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':# 递归终止条件if not root or root == p or root == q:return root# 后序遍历left = self.lowestCommonAncestor(root.left, p, q)right = self.lowestCommonAncestor(root.right, p, q)# 左右子树都找到,当前节点是LCAif left and right:return root# 只在左子树找到elif left:return left# 只在右子树找到else:return right
Go实现
type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode}func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {// 递归终止条件if root == nil || root == p || root == q {return root}// 后序遍历left := lowestCommonAncestor(root.Left, p, q)right := lowestCommonAncestor(root.Right, p, q)// 左右子树都找到,当前节点是LCAif left != nil && right != nil {return root}// 只在左子树找到if left != nil {return left}// 只在右子树找到return right}
Rust实现
// Definition for a binary tree node.#[derive(Debug, PartialEq, Eq)]pub struct TreeNode {pub val: i32,pub left: Option<Rc<RefCell<TreeNode>>>,pub right: Option<Rc<RefCell<TreeNode>>>,}impl TreeNode {#[inline]pub fn new(val: i32) -> Self {TreeNode {val,left: None,right: None}}}use std::rc::Rc;use std::cell::RefCell;impl Solution {pub fn lowest_common_ancestor(root: Option<Rc<RefCell<TreeNode>>>, p: Option<Rc<RefCell<TreeNode>>>, q: Option<Rc<RefCell<TreeNode>>>) -> Option<Rc<RefCell<TreeNode>>> {// 处理空节点情况if root.is_none() {return None;}// 检查当前节点是否是p或qif root == p || root == q {return root;}let root_clone = root.clone();let left = Self::lowest_common_ancestor(root_clone.as_ref().unwrap().borrow().left.clone(),p.clone(),q.clone());let right = Self::lowest_common_ancestor(root.as_ref().unwrap().borrow().right.clone(),p,q);// 左右子树都找到,当前节点是LCAif left.is_some() && right.is_some() {return root;}// 只在左子树找到else if left.is_some() {return left;}// 只在右子树找到else {return right;}}}
C++实现
struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {}};class Solution {public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {// 递归终止条件if (root == nullptr || root == p || root == q) {return root;}// 后序遍历TreeNode* left = lowestCommonAncestor(root->left, p, q);TreeNode* right = lowestCommonAncestor(root->right, p, q);// 左右子树都找到,当前节点是LCAif (left != nullptr && right != nullptr) {return root;}// 只在左子树找到else if (left != nullptr) {return left;}// 只在右子树找到else {return right;}}};
算法流程与示例分析
典型案例分析
让我们通过一个具体示例来理解算法的执行过程。考虑如下二叉树,其中p=7,q=8:
算法执行步骤:
从根节点3开始递归,分别遍历左子树(5)和右子树(1)
在左子树5中继续递归,遍历左子树(6)和右子树(2)
节点6的左右子树均为空,返回空
节点2的左子树(7)找到p节点,返回7;右子树(4)返回空,因此节点2返回7
节点5的左子树返回空,右子树返回7,因此节点5返回7
在右子树1中递归,遍历左子树(0)和右子树(8)
节点0返回空,节点8找到q节点,返回8,因此节点1返回8
根节点3的左子树返回7,右子树返回8,因此根节点3是LCA,返回3
特殊情况处理
算法优雅地处理了多种特殊情况:
一个节点是另一个节点的祖先:当p是q的祖先时,算法会在遍历到p节点时直接返回p,因为此时p的子树中包含q,而p本身就是最深的公共祖先
一个节点就是根节点:当p或q中有一个是根节点时,根节点自然就是最近公共祖先
树退化为链表:即使二叉树退化为单链表结构,算法依然能正确找到LCA,此时时间复杂度仍为O(n)
复杂度分析与优化
时间复杂度
递归解法的时间复杂度为O(n),其中n是二叉树的节点数。在最坏情况下(如退化为链表的二叉树),我们需要访问树中的每个节点一次。
空间复杂度
空间复杂度为O(h),其中h是二叉树的高度。这是因为递归调用栈的深度取决于树的高度。在平衡二叉树中,h=log n;在最坏情况下(退化为链表),h=n。
非递归解法思路
虽然递归解法简洁优雅,但对于深度较大的二叉树可能导致栈溢出。非递归解法通常采用以下两种思路:
路径记录法:分别记录从根节点到p和q的路径,然后找出两条路径的最后一个公共节点
后序遍历迭代法:使用栈模拟后序遍历过程,记录每个节点的访问状态
路径记录法的实现需要额外O(n)的空间来存储路径,但可以有效避免栈溢出问题,适合处理大型二叉树。
实际应用与扩展
二叉搜索树中的LCA
如果题目中的二叉树是二叉搜索树(BST),我们可以利用BST的特性(左子树所有节点值小于根节点,右子树所有节点值大于根节点)来优化算法:
// BST中的LCA优化算法public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 如果当前节点值大于p和q的值,LCA在左子树if (root.val > p.val && root.val > q.val) {return lowestCommonAncestor(root.left, p, q);}// 如果当前节点值小于p和q的值,LCA在右子树else if (root.val < p.val && root.val < q.val) {return lowestCommonAncestor(root.right, p, q);}// 否则当前节点就是LCAelse {return root;}}
这种方法的时间复杂度仍为O(n),但在平衡BST中平均情况下可以达到O(log n)。
多叉树的LCA问题
在多叉树中寻找LCA需要对算法进行扩展,通常采用以下策略:
记录每个节点的父节点,然后使用类似链表相交的方法寻找LCA
扩展后序遍历,对每个节点统计其子树中包含的目标节点数量
实际应用场景
LCA问题在实际应用中非常广泛:
家谱树分析:在家族树中寻找两个人的最近共同祖先
编译器设计:在抽象语法树(AST)中确定两个节点的作用域关系
版本控制系统:Git等版本控制系统使用LCA算法寻找两个分支的最近共同祖先版本
生物信息学:在进化树中确定两个物种的最近共同祖先
总结与思考
"二叉树的最近公共祖先"问题虽然简单,却很好地体现了二叉树遍历和递归思想的精髓。通过五种语言的实现对比,我们可以看到不同编程语言在处理递归和指针/引用时的特点:
Java和C++使用显式的指针/引用
Python和Go通过简洁的语法简化了递归实现
Rust的所有权模型要求更细致的引用管理
这一问题的解决思路可以推广到许多类似的树结构问题中,如"二叉树的直径"、"二叉树中的最大路径和"等。掌握LCA问题的解法,将为处理更复杂的树结构问题打下坚实基础。
在实际面试中,面试官往往会从基础的LCA问题出发,逐步深入到更复杂的扩展场景,如要求处理带父指针的二叉树、多叉树,或限制空间复杂度等。因此,我们不仅要记住算法实现,更要理解其背后的思想本质,才能灵活应对各种变化。
最后,值得一提的是,LCA问题的解法中蕴含着分治思想的火花——将复杂问题分解为简单子问题,通过解决子问题来构建原问题的解。这种思想在算法设计中具有普遍意义,是每个程序员都应该掌握的核心思维方法。

