题目描述
思路解析
一句话答案:LeetCode 236 二叉树的最近公共祖先的最优解是一次后序递归:在子树里找 p 和 q,碰到目标或空就原样返回;若左右子树各返回一个非空结果,当前节点就是两条路径第一次交汇的地方,即 LCA,否则把非空的那边继续上传。每个节点只访问一次,时间 O(n)、空间 O(h) 递归栈。
最近公共祖先到底是什么
给定二叉树中的两个节点 p 和 q,最近公共祖先(LCA)是同时罩住两者、且层级最深的那个节点。有一个容易忽略的定义细节:节点可以是它自己的祖先——如果 p 本身就在 q 的上方,那 p 自己就是答案。这个细节直接决定了递归里「碰到 p 或 q 就立刻返回」这一步是安全的。
为什么不先求两条路径再比对
最直觉的做法是分别找出根到 p、根到 q 的两条路径,再从头对比,最后一个相同的节点就是 LCA。这个思路完全正确,但要遍历两趟、还要额外存两条路径。它的价值在于点破了问题的本质:LCA 就是两条根到目标路径的分岔点。递归解法的妙处,是把「找路径」和「比路径」压进同一趟遍历里完成。
递归函数的返回值到底代表什么
递归函数的语义是:在当前子树里搜索 p 和 q,返回找到的东西。结果只有三种:子树里一个目标都没有,返回空;只找到 p 和 q 中的一个,就把那个节点往上传;两个都在这棵子树里,返回它们的 LCA。递归时碰到空或者碰到 p、q 本身就直接返回自己——即使另一个目标还藏在它下面也没关系,因为那种情况当前节点本来就是两者的公共祖先,返回自己恰好正确。
为什么左右都非空时当前节点就是答案
按后序的方式先问左子树、再问右子树,两边的答案都拿到后再裁决。若左右各返回一个非空节点,说明 p、q 分居当前节点的两侧,这里是两条向上路径第一次汇合的位置;任何更深的节点都不可能同时罩住分居两边的目标,所以当前节点就是最近的那个公共祖先。若只有一边非空,说明目标(或已经在下面算出的 LCA)全在那一边,原样上传即可;两边都空就传空。
这也解释了为什么必须是后序遍历:父亲的裁决完全依赖左右孩子的搜索结果,孩子不先算完,父亲无从判断。
复杂度多少,还有哪些追问
每个节点恰好被访问一次,时间 O(n);递归栈深等于树高 h,平衡树是 O(log n),最坏退化成链是 O(n)。
两个常见追问:若不保证 p、q 都在树里,要在后序里额外用两个布尔量确认两者真的都被找到,否则交汇点不算数;若树是二叉搜索树(LeetCode 235),可以利用有序性从根往下走——p、q 都比当前小就走左,都比当前大就走右,第一次分居两侧的节点即 LCA,只需 O(h) 且不必搜索两边。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这一句,下面每个节点都在套它。蓝色 = 目标 p/q,橙色 = 正在访问的节点,绿色 = 子树确认含目标。
访问 节点3:不是目标,先问左子树有没有 p 或 q。
访问 节点5:不是目标,先问左子树有没有 p 或 q。
访问 节点6:它正是目标 6!命中,把自己往上回传。
节点6 回传「找到了 6」,沿递归栈往上冒。
节点5 的左子树返回 节点6(找到了),再问右子树。
访问 节点2:不是目标,先问左子树有没有 p 或 q。
访问 节点7:叶子节点,左右都没有 p/q,回传 null。
节点7:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
节点2 的左子树返回 null(没找到),再问右子树。
访问 节点4:它正是目标 4!命中,把自己往上回传。
节点4 回传「找到了 4」,沿递归栈往上冒。
节点2:只有右边有目标,把「4」原样往上传,自己不是 LCA。
节点5:左子树有 6、右子树有 4 —— p 和 q 分居两侧,当前节点就是最近公共祖先!
节点3 的左子树返回 节点5(找到了),再问右子树。
访问 节点1:不是目标,先问左子树有没有 p 或 q。
访问 节点0:叶子节点,左右都没有 p/q,回传 null。
节点0:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
节点1 的左子树返回 null(没找到),再问右子树。
访问 节点8:叶子节点,左右都没有 p/q,回传 null。
节点8:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
节点1:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
节点3:只有左边有目标,把「5」原样往上传,自己不是 LCA。
递归回到节点 5 时,它的左子树带回了 6、右子树带回了 4 —— p、q 分居两侧,最近公共祖先就是 5(绿色)。
「一个节点也是自己的祖先」是本题最易漏的边界,第一句判断已覆盖。
两个高频追问:一个收紧前提,一个换 BST 提速。
参考代码
def lowestCommonAncestor(root, p, q): if root is None or root is p or root is q: return root # 命中目标或到底,原样返回 left = lowestCommonAncestor(root.left, p, q) right = lowestCommonAncestor(root.right, p, q) if left and right: return root # 左右各找到一个 → 当前就是 LCA return left if left else right # 否则往有的那边上传复杂度
- 时间:O(n),每个节点恰好被递归访问一次
- 空间:O(h),递归栈深 = 树高 h,最坏 O(n)(退化成链)
面试追问把动画讲成自己的话
追问若不保证 p、q 一定都在树里,怎么改?
追问是二叉搜索树(LC235)能更快吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
路径总和 III
LeetCode 437 · 中等 · 沿着 树 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题