LEETCODE 236Medium

二叉树的最近公共祖先

递归返回值只回答一个问题——这棵子树里有没有 p 或 q;左右都给出肯定答复的最深节点,就是 LCA。

问题拆解

在一棵普通二叉树里找 pq 的最近公共祖先(LCA)。注意题目允许节点是自己的祖先,所以当 p 本身是 q 的祖先时,答案就是 p

朴素做法是先各自找出根到 p、根到 q 的路径,再比对两条路径最后一个相同的节点。能做,但要额外存路径,而且“找路径”本身就得递归一遍。其实可以把判断融进一次后序遍历:站在任意节点上问左右子树“你们那边有没有 pq”,如果两边都说有,pq 必然分居两侧,当前节点就是它们最深的汇合点。

让递归函数的返回值携带明确语义:在这棵子树里找到了 pq 就返回那个节点,否则返回空。LCA 是自底向上第一个“左右都非空”的节点。

后序递归:左右各找一遍

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) {
        return root; // 碰到 p 或 q 就不必再往下,直接上报
    }
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    if (left != null && right != null) {
        return root; // p、q 分居两侧,当前节点就是 LCA
    }
    return left != null ? left : right; // 只命中一边,把结果继续上传
}
def lowestCommonAncestor(root, p, q):
    if root is None or root is p or root is q:
        return root  # 碰到 p 或 q 就不必再往下,直接上报
    left = lowestCommonAncestor(root.left, p, q)
    right = lowestCommonAncestor(root.right, p, q)
    if left and right:
        return root  # p、q 分居两侧,当前节点就是 LCA
    return left or right  # 只命中一边,把结果继续上传
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
    if root == nil || root == p || root == q {
        return root // 碰到 p 或 q 就不必再往下,直接上报
    }
    left := lowestCommonAncestor(root.Left, p, q)
    right := lowestCommonAncestor(root.Right, p, q)
    if left != nil && right != nil {
        return root // p、q 分居两侧,当前节点就是 LCA
    }
    if left != nil {
        return left
    }
    return right
}
use std::rc::Rc;
use std::cell::RefCell;

// 题目保证节点值互不相同,Rust 版用值比较代替指针比较
pub fn lowest_common_ancestor(
    root: Option<Rc<RefCell<TreeNode>>>,
    p: Option<Rc<RefCell<TreeNode>>>,
    q: Option<Rc<RefCell<TreeNode>>>,
) -> Option<Rc<RefCell<TreeNode>>> {
    let node = root?;
    let pv = p.as_ref().unwrap().borrow().val;
    let qv = q.as_ref().unwrap().borrow().val;
    let v = node.borrow().val;
    if v == pv || v == qv {
        return Some(node); // 碰到 p 或 q 就直接上报
    }
    let left = Self::lowest_common_ancestor(node.borrow().left.clone(), p.clone(), q.clone());
    let right = Self::lowest_common_ancestor(node.borrow().right.clone(), p, q);
    match (left, right) {
        (Some(_), Some(_)) => Some(node), // 分居两侧,当前节点就是 LCA
        (l, r) => l.or(r),
    }
}

返回值的语义要咬死:它不是“这棵子树的 LCA”,而是“这棵子树里找到的 pq(或已确定的 LCA)”。三种情况各自成立——左右都非空,说明两个目标第一次在这里汇合,返回当前节点;只有一边非空,说明两个目标都在那一边(或只找到了一个),原样上传;都为空,返回空。

最容易被忽略的是第一行的短路:一旦 root 就是 pq,立刻返回,不再往下搜。这正确处理了“pq 的祖先”的情形——另一个目标一定在它的子树里,不搜也知道答案是它。而汇合点一旦确定,leftright 中只有一个非空,这个答案会被一路原样传回根,不会被中途覆盖。

复杂度

指标 复杂度 原因
时间 O(n) 每个节点最多访问一次
空间 O(h) 递归栈深度为树高,最坏退化成 O(n)

可以迁移的模式

  • 给递归返回值定一个单一、明确的语义,父节点只负责组合子结果;
  • “两边都命中才在此汇合”的判断,天然要求后序(先左右后自身);
  • 目标节点本身可以作为递归终止条件,顺带覆盖“一个是另一个祖先”的边界。

树上求“汇合点”类的问题,几乎都是这套后序 + 返回值语义的变体。