二叉搜索树的最近公共祖先
把当前节点想成带数字路牌的岔路口:两个目标都小就一起向左,都大就一起向右;一旦开始分路,当前节点就是最低公共祖先。
在一棵 BST 中找到同时是 p、q 祖先且位置最低的节点。
利用 BST 的有序性,不遍历无关子树,沿一条搜索路径直接定位答案。
同小向左,同大向右;其余情况包含分岔或命中目标,直接返回当前节点。
先说结论:这道题到底解决什么
已经站在某个 BST 节点时,怎样只凭 p、q 与当前值的大小关系,证明整棵左侧或右侧都不可能包含答案?
中心结论:同小向左,同大向右;其余情况包含分岔或命中目标,直接返回当前节点。
- 1.为什么 p、q 分居当前节点两侧时,当前节点就是最近公共祖先?
- 2.为什么 p、q 同在一侧时,可以放心丢弃另一整棵子树?
- 3.为什么答案可以恰好等于 p 或 q?
完整题目与题意拆解
给定一棵二叉搜索树,以及树中的两个节点 p 和 q,返回它们的最近公共祖先。
最近公共祖先是同时覆盖 p、q 的祖先中深度最大的一个。题目采用的祖先定义允许节点是自己的祖先,因此答案可以等于 p 或 q。
- • 所有节点值唯一,p 与 q 均存在于树中。
- • BST 左子树值都小于当前节点,右子树值都大于当前节点。
- • 答案不要求严格高于两个目标。
输入:root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出:6
解释:2 在 6 左边,8 在 6 右边,6 是两条搜索路线最低的分岔点。不要先想“遍历整棵树找两条路径”。站在当前节点,只比较 p.Val、q.Val 与 root.Val,就能判断两个目标是否仍在同一侧。
如果它们已经分居两侧,当前节点还能覆盖两者,而任何更低的单侧节点都不可能同时覆盖两者,所以当前节点就是最近公共祖先。
先看懂“最低公共路口”
观察 p=2、q=8 与根 6 的位置。先判断谁能同时覆盖两人,再判断还能不能继续向下。
第一层方案:暴力做法
可以分别从根搜索到 p、q,保存两条路径,再找到最后一个相同节点。这个方法正确,但需要两次搜索和 O(h) 路径空间。
也可以直接套用 LC236 的普通二叉树后序递归,时间最坏 O(n)。它忽略了 BST 的有序性,不是这题最有价值的解法。
path(root,p) = [6,2]
path(root,q) = [6,8]
最后一个公共节点 = 6普通树搜索为什么浪费了 BST 路牌
对照 LC236 的左右搜索:如果忽略大小关系,就会访问本可以整棵排除的分支。
优化方向:每次比较后,搜索范围只保留左子树、右子树或立即结束。因此执行路线是一条从根向下的链,而不是整棵树的遍历。
整体地图:先做什么,再做什么
这题先不要从递归模板出发。第一步只定义 LCA:能够同时覆盖 p、q 的祖先中,离它们最近的那个。第二步读取 BST 的有序性:左子树所有值更小,右子树所有值更大。
扫描时只维护当前 root。若 p、q 都比 root 小,答案必在左侧;若都大,答案必在右侧;其余情况表示两者在这里分开,或者 root 已经命中其中一个目标。
- • 输入性质:这是 BST,而不是任意二叉树。
- • 循环状态:root 是当前仍可能包含两个目标的最低候选子树根。
- • 停止条件:分岔或命中目标时返回 root。
BST 路牌到底提供了什么信息
BST 在每个节点都提供一个方向路牌:比当前值小的节点只能出现在左子树,比当前值大的节点只能出现在右子树。比较 p、q 两个值,相当于同时询问两位目标会走向哪条路。
如果两者给出相同方向,当前节点还不是最低公共节点,因为更低的一侧仍可能同时覆盖它们。如果方向不同,任何更低节点都只能位于某一侧,不可能继续同时覆盖两人。
root=6, p=2, q=8 → 2<6<8,方向分开,答案是 6
root=6, p=2, q=4 → 2<6 且 4<6,答案继续去左侧一左一右时,方向在当前节点分开
逐步比较两个目标,看到“同侧继续、分侧停止”不是口号,而是由值域决定。
如何把三种关系收敛成一个循环
每轮先同时比较 p.Val、q.Val 与 root.Val。两个严格小于才向左,两个严格大于才向右。剩余情况统一返回 root,其中既包含一左一右,也包含 root==p 或 root==q。
严格比较很重要。若把条件写成小于等于,root 命中目标时还会继续向下,反而越过正确答案。循环在 root 为空时结束只是防御性处理,题目通常保证目标存在。
- • 两个都小:`root = root.Left`。
- • 两个都大:`root = root.Right`。
- • 其余情况:`return root`。
从 root 开始循环。先检查 p、q 是否都比 root 小,再检查是否都比 root 大;分别把 root 移到 Left 或 Right。
如果两种同侧条件都不成立,立即返回 root。题目保证 p、q 存在,所以正常输入会在某个节点停下。
构建三分判断,并覆盖自身为祖先
用 p=2、q=4 观察同侧下探,再看 root 命中 p 时为什么直接返回。
核心难点:为什么分岔处不会漏掉更低答案
设 p 位于当前节点左侧、q 位于右侧。当前节点显然同时是两者祖先。若还存在一个更低的公共祖先 x,x 必须位于当前节点某一棵子树内。
但左子树中的 x 不可能成为右侧 q 的祖先,右子树中的 x 也不可能成为左侧 p 的祖先,因此更低公共祖先不存在。当前节点就是最低的那个。
同侧移动也安全:当 p、q 都小于 root 时,两者都在左子树,任何包含它们的公共祖先如果低于 root,也只能在左子树。丢弃右子树不会丢答案。
- • 两目标都小于当前节点时,它们和 LCA 必然都在左子树;都大时同理在右子树。
- • 不在同一侧时,当前节点能同时覆盖两者,而任何更低的单侧节点都不能。
- • 命中 p 或 q 也属于停止情况,符合节点可以是自身祖先的定义。
完整执行过程
先看 p=2、q=8 在根节点分岔,再换成 p=2、q=4 观察同侧下探。重点看每次比较为什么能安全丢弃一整侧。
- 1主例 p=2、q=8,从 root=6 开始;两个目标分别小于和大于 6。
- 2方向在 6 分开,继续向左会丢掉 8,继续向右会丢掉 2,因此立即返回 6。
- 3对照例 p=2、q=4,在 root=6 时两者都小,当前 root 移到左孩子 2。
- 4到 root=2 时命中 p;q=4 位于 2 的子树内,所以 2 同时覆盖两者且已经最低。
- 5整个过程只沿根到答案的一条路径移动,没有扫描被排除的子树。
两个用例串成一条完整决策轨迹
先走分岔用例,再走同侧用例;拖动时间轴比较每次 root 变化的理由。
把动画和 Go 代码逐行对应
每一帧使用稳定的语义行 ID,不依赖容易漂移的数字行号。先观察分支为什么成立, 再看对应代码,而不是先背模板。
三类画面状态对应三个 Go 分支
观察比较结果、root 移动和 return 高亮是否同步,重点区分严格小于与命中目标。
LCA 的核心是“最低的共同覆盖点”,不是离根最近的点。
BST 左子树所有节点都比当前节点小。
向左会丢掉 8,向右会丢掉 2;没有更低节点能同时覆盖两者。
答案不可能出现在比 6 更大的右子树,保留它只会增加无效搜索。
BST 性质已经证明被丢弃区域不可能含答案。
祖先定义允许节点包含自己;2 是同时覆盖 2 与 4 的最低节点。
LCA 判断依赖两个目标相对当前节点的联合位置。
普通二叉树没有值与位置的约束,LC236 必须询问左右子树并等待返回信号。
没有递归栈,也没有保存路径;平衡 BST 较短,链状 BST 最坏较长。
完整 Go 提交代码与最小测试
1func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {2 for root != nil {3 if p.Val < root.Val && q.Val < root.Val {4 root = root.Left5 } else if p.Val > root.Val && q.Val > root.Val {6 root = root.Right7 } else {8 return root9 }10 }11 return nil12}// 分居两侧:答案在根
lowestCommonAncestor(root, node2, node8) // 6
// 同在左侧:必须继续下探
lowestCommonAncestor(root, node2, node4) // 2
// 目标节点本身就是祖先
lowestCommonAncestor(root, node4, node3) // 4
// 退化 BST:仍正确,时间退化为 O(n)
// 1 -> 2 -> 3, p=2, q=3 => 2正确性与复杂度
每轮只向下一层并丢弃另一侧,最多走过树高 h;平衡树约 O(log n),链状树最坏 O(n)。
迭代只维护 root、p、q 三个指针,不使用递归栈或路径数组。
最容易写错的地方
把普通二叉树也按节点值剪枝。
只比较 p 或 q 中的一个,无法判断是否分岔。
认为 LCA 不能等于 p 或 q。
把 O(h) 一概写成 O(log n),忽略 BST 可能退化。
最后复盘:带走逻辑链
- 1.BST 的有序性把全树搜索缩成单路径下探。
- 2.同小向左,同大向右,其余情况立即返回。
- 3.分岔与命中目标统一落在同一个 else 分支。
- 4.迁移题:LC236 普通二叉树 LCA、LC700 BST 搜索。
- 1.利用 BST 左小右大的性质,从根开始同时比较 p、q。
- 2.两者都小就向左,都大就向右,否则说明分岔或命中目标,返回当前节点。
- 3.每次只保留一棵子树,所以时间 O(h),平衡时 O(log n)、最坏 O(n)。
- 4.使用迭代只维护当前指针,额外空间 O(1)。