一文彻底搞懂二叉树的最近公共祖先(LCA)问题|LeetCode 236 题解

123 阅读4分钟

🌳 问题回顾:什么是最近公共祖先(LCA)?

给定一棵二叉树和两个节点 pq,找到它们的最近公共祖先(Lowest Common Ancestor, LCA)

定义:LCA 是同时为 pq 祖先的深度最大的节点。一个节点可以是自身的祖先。

例如:

        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4
  • LCA(5, 1) = 3(分属左右子树)
  • LCA(5, 4) = 5(4 是 5 的后代)

🔍 初步思考:能用前序或中序吗?

很多初学者会问:为什么不能用前序遍历?

假设我们从前序开始(根 → 左 → 右):

  • 在根节点 3,我们还不知道 pq 到底在左子树还是右子树;
  • 即使我们记录路径,也需要额外空间存储从根到每个节点的路径,再比较;
  • 更麻烦的是,我们无法在访问当前节点时,就确定它是不是“最近”的公共祖先

✅ 关键洞察:LCA 的判定依赖于子树的信息——只有知道左右子树是否包含 pq,才能判断当前节点是否为 LCA。

这就引出了核心原则:

必须先知道子树的结果,再决定当前节点的行为。

而这,正是 后序遍历(左 → 右 → 根) 的本质!


🔄 为什么后序遍历 + 回溯是天然选择?

Step 1:我们需要“自底向上”的信息

想象你在树的最底层(比如叶子节点):

  • 如果你找到了 p,你会告诉父节点:“我这边有 p!”
  • 如果你没找到,就告诉父节点:“我这边啥也没有。”

父节点收集左右孩子的反馈后,就能判断:

  • 如果左边说有,右边也说有 → 那我就是 LCA!
  • 如果只有一边有 → 我把那个结果继续往上传。

这个过程,就是典型的 回溯(Backtracking)先深入到底,再带着结果一层层返回

Step 2:后序遍历天然支持“先处理子树,再处理根”

后序遍历的执行顺序保证了:

  • 在处理当前节点 root 之前,root.leftroot.right 的递归已经完成;
  • 我们可以直接拿到左右子树的“搜索结果”。

这正是 LCA 问题所需要的 信息聚合时机

💡 所以,并不是“我们选择了后序遍历”,而是 问题本身的性质决定了必须用后序遍历


🧠 递归函数的设计哲学

我们定义递归函数:

function dfs(root, p, q)

语义:在以 root 为根的子树中,如果包含 pq,则返回其中“有意义”的节点(可能是 LCA,也可能是 p/q 本身);否则返回 null

✅ 终止条件:何时停止递归?

if (root === null || root === p || root === q) {
    return root;
}
  • root === null:空节点,无意义,返回 null
  • root === proot === q:找到了目标,立即返回该节点。

这不是为了“判断叶子”,而是为了 提前终止无效搜索——一旦命中目标,无需再看其子树(因为题目保证节点存在,且 LCA 要么是它自己,要么在其祖先中)。

🔁 递归与回溯:收集子树信息

const left = dfs(root.left, p, q);
const right = dfs(root.right, p, q);

现在,我们有了左右子树的“报告”:

情况含义当前节点应返回
leftright 都非空pq 分居两侧root(这就是 LCA!)
left 非空pq 都在左子树left(LCA 在左边)
right 非空pq 都在右子树right(LCA 在右边)
都为空当前子树不含 pqnull

这个逻辑,完美体现了 “子树结果决定当前行为” 的后序思想。


✅ 完整代码(JavaScript)

var lowestCommonAncestor = function(root, p, q) {
    const dfs = (root, p, q) => {
        // 基线条件:空节点或命中目标
        if (root === null || root === p || root === q) {
            return root;
        }

        // 先递归左右子树(后序的第一步)
        const left = dfs(root.left, p, q);
        const right = dfs(root.right, p, q);

        // 根据子树结果决定当前返回值(后序的第二步)
        if (left && right) {
            return root; // 分居两侧,当前是 LCA
        }
        return left || right; // 哪边有结果就传哪边
    };

    return dfs(root, p, q);
};

💡 小技巧:最后可以用 return left || right 简化逻辑,因为 null 是 falsy 值。


🌟 总结:LCA 与后序遍历的必然联系

关键点说明
为什么必须后序?LCA 的判定依赖子树信息,只有后序能先获得左右结果
回溯体现在哪?从叶子带回“是否找到目标”的信息,逐层向上传递
终止条件的意义提前返回目标节点,避免无效遍历,提升效率
返回值的含义不一定是 LCA,也可能是 p/q,由上层判断

📚 延伸思考

  • 如果是 二叉搜索树(BST) ,能否利用有序性改用前序?(答案:可以!)
  • 如果要找 k 个节点的 LCA,逻辑如何扩展?
  • 如果树中 可能不存在 pq,如何验证结果有效性?

这些问题,都建立在对“信息回传”和“遍历顺序”深刻理解的基础上。


✅ 结语

LCA 问题看似简单,却蕴含着递归设计的精髓:不是我们选择了某种遍历方式,而是问题本身的结构决定了最优解法

当你下次遇到“需要子树信息才能决策”的树形问题时,请记住:后序遍历,往往是答案所在