目录

题目描述

1650. 二叉树的最近公共祖先 III

题意分析

给两个节点 pq,求它们的最近公共祖先。与 236、1644 最大的不同是函数签名里根本没有 root:拿不到树根,也就没法自顶向下遍历。作为补偿,每个节点多了一个 parent 指针,根节点的 parent 为空。

少了根、多了父指针,这两件事合起来把题目彻底换了一副骨架。自顶向下的递归失去了入口,而自底向上的行走成了唯一可行的方向:从 p 沿 parent 一路往上,会依次经过 p 的所有祖先,直到根;从 q 出发同理。

于是可以把每个节点到根的路径看成一条链表p → p.parent → … → root → nullq → q.parent → … → root → null。两条链最终都走到同一个根,也就是说它们必然共享一段公共后缀。最近公共祖先,正是这段公共后缀的第一个节点——也就是两条链表的相交点。

题目保证 pq 都存在于同一棵树中,所以相交点一定存在,不必考虑无解。但 p 可能是 q 的祖先(此时答案是 p 自己),p 也可能就等于 q(答案还是它自己),这两种退化情况必须被主逻辑自然覆盖。

两条链的长度分别是两个节点的深度加一,通常不相等,这个长度差是设计算法时必须消化掉的核心难点。

解法:双指针收缩边界

核心思路

沿 parent 从节点走到根,会得到一条单链。p 链和 q 链从最近公共祖先开始共享同一后缀,所以问题与“相交链表”完全相同;难点只是两条链在公共后缀前的长度不同。

令指针 ap 出发,走到空后改从 q 出发;b 对称地从 q 出发,走到空后改从 p 出发。这样 a 依次走过 p 链和 q 链,b 依次走过 q 链和 p 链,两者总路程相同,起点到公共后缀的长度差被自动抵消。

循环不变量是两个指针始终走过相同步数。换链后,它们在各自第二段路径中拥有相同的剩余距离,因此会在公共后缀的第一个节点相遇;该节点正是最近公共祖先。题目保证两点属于同一棵树,所以一定存在这个非空交点。

p == q 时零步相遇;一方是另一方祖先时,也会在祖先节点相遇,无需特判。

解题步骤

  1. 初始化 a = pb = q,节点自身也可能是答案。
  2. a != b 时同步推进两个指针。
  3. 指针非空就走到 parent;为空就切换到另一个指针的起点。
  4. 两者相同时返回该节点。

例如 p = 5q = 4,且 5 是 4 的祖先。两条父链长度不同,但换链后都会走过两条链,最终在节点 5 相遇。

比较必须使用节点引用。即使节点值可能相同,也不代表它们是同一个祖先节点。

代码实现

class Solution {
    public Node lowestCommonAncestor(Node p, Node q) {
        Node a = p;
        Node b = q;
        while (a != b) {
            a = a == null ? q : a.parent;
            b = b == null ? p : b.parent;
        }
        return a;
    }
}
func lowestCommonAncestor(p *Node, q *Node) *Node {
	a, b := p, q
	for a != b {
		if a == nil {
			a = q
		} else {
			a = a.Parent
		}
		if b == nil {
			b = p
		} else {
			b = b.Parent
		}
	}
	return a
}

复杂度分析

  • 时间复杂度:$O(h_p+h_q)$,两个指针至多各走完两条父链;最坏可写为 $O(h)$,其中 $h$ 是树高。
  • 空间复杂度:$O(1)$,只使用两个节点指针。

关键点总结

  • 父指针把“节点到根”变成链表,LCA 就是两条链的第一个公共节点。
  • 换链让两个指针各走一遍两条路径,自动抵消深度差。
  • 空指针时必须切换到对方起点,而不是对方父节点。
  • 使用引用相等判断节点,不能比较 val
  • p == q 和祖先包含关系都由同一循环自然覆盖。

易错点总结

  • p.parentq.parent 开始会跳过节点自身;当 p 是答案时会返回更高祖先。
  • 换链时回到自己的起点,无法抵消长度差,可能永不相遇。
  • 换到对方的 parent 会让两条拼接路径长度再次错位。
  • 比较节点值而非引用,会在不同但同值的节点上提前停止。
  • 两个指针必须每轮各前进一步,否则“已走步数相同”的不变量失效。

相似题目

题目 难度 考察点
160. 相交链表 简单 与本题完全同构,把父指针换成 next,双指针换链的原型就在这里
236. 二叉树的最近公共祖先 中等 有根无父指针,只能自顶向下后序递归,空间是 $O(h)$ 的调用栈
235. 二叉搜索树的最近公共祖先 中等 借助有序性一路比大小下探,迭代实现同样能做到 $O(1)$ 空间但方向相反
1644. 二叉树的最近公共祖先 II 中等 目标可能不存在,必须走完全树统计存在性,不能提前返回
1676. 二叉树的最近公共祖先 IV 中等 目标扩展成一组节点,用集合判定命中,递归骨架与 236 一致
142. 环形链表 II 中等 同样是常数空间的双指针,但靠快慢速度差而非路径长度对齐,推导方式完全不同
1483. 树节点的第 K 个祖先 困难 沿父链上跳的加速版,用倍增表把单次上跳 $O(h)$ 降到 $O(\log h)$
面试题 02.07. 链表相交 简单 与 160 同题,可直接套用换链写法,常被拿来和本题对照提问