LeetCode 1650 二叉树最近公共祖先(带父指针):python3 题解


题目链接(LeetCode 上这道题是收费的,这个链接免费):1650. 二叉树最近公共祖先


1. 题目理解

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

关键特点
与普通的二叉树 LCA 问题不同,这道题的节点定义中包含了一个指向父节点的指针 parent

class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None
        self.parent = None  # 关键:可以向上遍历

什么是最近公共祖先?
想象这棵树是一个家族谱系。pq 是两个人。它们的公共祖先是它们共同的长辈。而“最近”意味着在所有共同长辈中,辈分最高(离它们最近)的那一位。

  • 注意:一个节点可以是它自己的祖先。例如,如果 pq 的父节点,那么 p 就是 LCA。

核心思路转换
因为每个节点都有 parent 指针,我们可以从 pq 开始,一直向上走直到根节点。
这实际上将“树的问题”转化为了"链表相交问题"。

  • p 到根节点的路径,是一条链表。
  • q 到根节点的路径,是另一条链表。
  • 这两条链表在“最近公共祖先”处开始合并(相交),直到根节点都是重合的。

我们的任务就是找到这个相交点


2. 解法一:哈希集合法(直观易懂)

思路

这是最容易想到的方法。

  1. 从节点 p 开始,沿着 parent 指针一直向上走,把路径上经过的所有节点都存入一个集合(Set)中。
  2. 然后从节点 q 开始,沿着 parent 指针向上走。
  3. 在走的过程中,检查当前节点是否已经在集合里。
  4. 第一个在集合里发现的节点,就是 pq 路径的第一个交点,即最近公共祖先。

图解

假设树结构如下:

      3 (Root)
     / \
    5   1
   / \
  6   2
     / \
    7   4
  • p = 7, q = 4
  • p 的路径:7 -> 2 -> 5 -> 3 (存入集合)
  • q 的路径:4 -> 2 (检查集合)
  • q 走到 2 时,发现 2 已经在集合里了(因为 p 的路径经过 2)。
  • 返回 2

复杂度

  • 时间复杂度\(O(H)\),其中 \(H\) 是树的高度。最坏情况下需要遍历到根节点。
  • 空间复杂度\(O(H)\),需要用集合存储 p 到根的路径。

3. 解法二:双指针法(最优解,空间 O(1))

思路

这是一个非常经典的技巧,常用于解决“两个链表寻找交点”的问题。

我们定义两个指针 ab,分别指向 pq

  1. ap 出发向上走,bq 出发向上走。
  2. 如果 a 走到了根节点(a.parentNone),让它“传送”到 q 的起始位置继续走。
  3. 如果 b 走到了根节点(b.parentNone),让它“传送”到 p 的起始位置继续走。
  4. a == b 时,相遇的节点就是 LCA。

为什么这样能行?

这利用了路径长度相等的原理。

  • p 到 LCA 的距离为 \(A\)
  • q 到 LCA 的距离为 \(B\)
  • 设 LCA 到根节点的距离为 \(C\)

指针 a 的路线:先走 \(p \to LCA \to Root\) (距离 \(A+C\)),然后跳到 \(q\),再走 \(q \to LCA\) (距离 \(B\))。总路程 = \(A + C + B\)
指针 b 的路线:先走 \(q \to LCA \to Root\) (距离 \(B+C\)),然后跳到 \(p\),再走 \(p \to LCA\) (距离 \(A\))。总路程 = \(B + C + A\)

因为 \(A+C+B = B+C+A\),所以两个指针走的总步数是一样的。它们会在第二次遍历时的 LCA 处相遇。

即使其中一个节点是另一个的祖先(例如 pq 的祖先),这个逻辑依然成立(此时 \(A=0\)\(B=0\))。

复杂度

  • 时间复杂度\(O(H)\),两个指针最多各走两遍树的高度。
  • 空间复杂度\(O(1)\),只需要两个指针变量,不需要额外存储空间。

4. 代码实现 (Python 3)

下面提供完整的代码,包含两种解法。建议优先掌握解法二(双指针),因为它更优雅且节省内存。

# 定义节点结构,方便本地测试理解
class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None
        self.parent = None

class Solution:
    """
    解法一:哈希集合法
    思路:记录 p 的所有祖先,然后检查 q 的祖先是否在记录中。
    优点:逻辑非常直观,容易理解。
    缺点:需要 O(H) 的额外空间。
    """
    def lowestCommonAncestor_set(self, p: 'Node', q: 'Node') -> 'Node':
        # 创建一个集合用来存储 p 节点向上的所有路径节点
        path_set = set()
        
        # 1. 从 p 开始向上遍历,直到根节点 (parent 为 None)
        curr = p
        while curr:
            path_set.add(curr)
            curr = curr.parent
            
        # 2. 从 q 开始向上遍历,遇到的第一个在集合中的节点即为 LCA
        curr = q
        while curr:
            if curr in path_set:
                return curr
            curr = curr.parent
            
        # 理论上题目保证 p, q 在树中,一定能找到,这里返回 None 以防万一
        return None

    """
    解法二:双指针法(推荐)
    思路:利用 a + b = b + a 的原理,消除两个节点到根节点的距离差。
    优点:空间复杂度 O(1),代码简洁。
    缺点:逻辑稍微需要转个弯。
    """
    def lowestCommonAncestor(self, p: 'Node', q: 'Node') -> 'Node':
        # 初始化两个指针,分别指向 p 和 q
        a, b = p, q
        
        # 当两个指针没有相遇时,继续循环
        # 如果 p 和 q 有公共祖先,它们一定会在某个节点相遇
        while a != b:
            # 如果 a 走到了尽头 (None),就让它从 q 开始重新走
            # 否则,继续向上走一步 (a.parent)
            a = a.parent if a else q
            
            # 如果 b 走到了尽头 (None),就让它从 p 开始重新走
            # 否则,继续向上走一步 (b.parent)
            b = b.parent if b else p
            
        # 当循环结束时,a 和 b 相等,即为最近公共祖先
        # 此时可能是在某个中间节点相遇,也可能都是 None (如果树不连通,但题目保证连通)
        return a

# ==========================================
# 以下代码用于本地测试,提交时不需要
# ==========================================
if __name__ == "__main__":
    # 构建示例 1 的树: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
    # 为了演示,我们手动构建节点关系
    nodes = {i: Node(i) for i in range(9)}
    
    # 建立连接 (val: parent_val) 简化构建过程
    # 3 是根
    # 5, 1 是 3 的孩子
    # 6, 2 是 5 的孩子
    # 0, 8 是 1 的孩子
    # 7, 4 是 2 的孩子
    
    # 设置 parent 关系
    nodes[5].parent = nodes[3]
    nodes[1].parent = nodes[3]
    nodes[6].parent = nodes[5]
    nodes[2].parent = nodes[5]
    nodes[0].parent = nodes[1]
    nodes[8].parent = nodes[1]
    nodes[7].parent = nodes[2]
    nodes[4].parent = nodes[2]
    
    sol = Solution()
    
    # 测试案例 1: p=5, q=1 -> LCA 应该是 3
    p1, q1 = nodes[5], nodes[1]
    res1 = sol.lowestCommonAncestor(p1, q1)
    print(f"示例 1 结果:{res1.val if res1 else None} (期望:3)")
    
    # 测试案例 2: p=5, q=4 -> LCA 应该是 5
    p2, q2 = nodes[5], nodes[4]
    res2 = sol.lowestCommonAncestor(p2, q2)
    print(f"示例 2 结果:{res2.val if res2 else None} (期望:5)")

5. 总结

特性 哈希集合法 双指针法
核心思想 记录路径,查找重合 路径长度互补,消除距离差
时间复杂度 \(O(H)\) \(O(H)\)
空间复杂度 \(O(H)\) \(O(1)\)
代码难度
推荐程度 ⭐⭐⭐ ⭐⭐⭐⭐⭐

建议
在面试或实际编程中,双指针法是更优的选择,因为它不需要额外的内存空间,且代码非常精简。理解双指针法的关键在于将其视为两个不同起点的链表寻找交点的问题。



posted @ 2026-03-10 14:19  MoonOut  阅读(8)  评论(0)    收藏  举报