最近公共祖先 图解题解
找最近公祖,其实你不用走两遍——让递归的回程帮你传消息。
找两个远房亲戚的最近共同祖先,就像在家谱里同时查两条血脉:不用各自把一整条族谱抄出来再逐行比对,更聪明的做法是让一个人拿着家谱从根往下走,遇到其中一方就往上回报消息。某一辈的祖先同时收到左边找到了一个、右边找到了另一个,那这一辈就是两人最近的共同节点——左右消息汇聚处,即是答案。
这道题到底在问什么
- 输入
- tree=[3,5,1,6,2,0,8,null,null,7,4], p=6, q=4
- 输出
- 5
最优解:为什么这么做
一句话答案: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) 且不必搜索两边。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这一句,下面每个节点都在套它。蓝色 = 目标 p/q,橙色 = 正在访问的节点,绿色 = 子树确认含目标。
- 4访问 节点3:不是目标,先问左子树有没有 p 或 q。
- 5访问 节点5:不是目标,先问左子树有没有 p 或 q。
- 6访问 节点6:它正是目标 6!命中,把自己往上回传。
- 7节点6 回传「找到了 6」,沿递归栈往上冒。
- 8节点5 的左子树返回 节点6(找到了),再问右子树。
- 9访问 节点2:不是目标,先问左子树有没有 p 或 q。
- 10访问 节点7:叶子节点,左右都没有 p/q,回传 null。
- 11节点7:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
- 12节点2 的左子树返回 null(没找到),再问右子树。
- 13访问 节点4:它正是目标 4!命中,把自己往上回传。
- 14节点4 回传「找到了 4」,沿递归栈往上冒。
- 15节点2:只有右边有目标,把「4」原样往上传,自己不是 LCA。
- 16节点5:左子树有 6、右子树有 4 —— p 和 q 分居两侧,当前节点就是最近公共祖先!
- 17节点3 的左子树返回 节点5(找到了),再问右子树。
- 18访问 节点1:不是目标,先问左子树有没有 p 或 q。
- 19访问 节点0:叶子节点,左右都没有 p/q,回传 null。
- 20节点0:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
- 21节点1 的左子树返回 null(没找到),再问右子树。
- 22访问 节点8:叶子节点,左右都没有 p/q,回传 null。
- 23节点8:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
- 24节点1:只有没有一边有目标,把「null」原样往上传,自己不是 LCA。
- 25节点3:只有左边有目标,把「5」原样往上传,自己不是 LCA。
- 26递归回到节点 5 时,它的左子树带回了 6、右子树带回了 4 —— p、q 分居两侧,最近公共祖先就是 5(绿色)。
完整代码(Python / C++ / Java)
Python
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 # 否则往有的那边上传C++
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);
if(left && right) return root; // 分居两侧,当前即 LCA
return left ? left : right; // 往有的那边上传
}Java
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);
if (left != null && right != null) return root; // 分居两侧 → LCA
return left != null ? left : right; // 往有的那边上传
}复杂度
时间
O(n)
每个节点恰好被递归访问一次
空间
O(h)
递归栈深 = 树高 h,最坏 O(n)(退化成链)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最近公共祖先 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
若不保证 p、q 一定都在树里,怎么改?+
后序遍历同时统计是否真的找到了 p 和 q(两个布尔),只有两者都为 true 才认这个交汇点,否则返回 null。
是二叉搜索树(LC235)能更快吗?+
能。利用 BST 有序:从根走,若 p、q 都比当前小走左、都比大走右,否则当前节点就是分叉点即 LCA,O(h) 且不必递归两边。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最近公共祖先 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。