LeetCode 236:二叉树的最近公共祖先——DFS 路径记录法详解

一、题目描述

给定一棵二叉树和其中两个节点 pq,找出它们的最近公共祖先。

最近公共祖先是同时作为 pq 祖先且深度最大的节点。一个节点也可以是它自己的祖先,因此当 p 位于 q 的祖先链上时,p 本身就可能是答案。

例如:

复制代码
          3
        /   \
       5     1
      / \   / \
     6   2 0   8
        / \
       7   4
  • p = 5q = 1 时,最近公共祖先是 3

  • p = 5q = 4 时,最近公共祖先是 5

本文严格采用一种直观思路:先通过 DFS 找出根节点到 pq 的两条路径,再比较路径中的节点。

二、核心思路:记录两条根节点路径

以查找节点 6 和节点 4 的最近公共祖先为例。

从根节点出发进行深度优先搜索,可以得到:

复制代码
根节点到 6 的路径:3 -> 5 -> 6
根节点到 4 的路径:3 -> 5 -> 2 -> 4

两条路径从根节点开始比较:

复制代码
3 == 3
5 == 5
6 != 2

最后一个相同节点是 5,所以 5 就是节点 6 和节点 4 的最近公共祖先。

整个算法可以分为两步:

  1. 使用 DFS 分别保存根节点到 pq 的路径;

  2. 同时遍历两条路径,记录最后一个相同节点。

三、DFS 如何记录路径

搜索过程中使用三个集合:

  • tempPath:保存 DFS 当前走过的临时路径;

  • pathToP:保存根节点到 p 的完整路径;

  • pathToQ:保存根节点到 q 的完整路径。

访问一个节点时,先把它加入 tempPath。如果当前节点就是 pq,便使用 addAll 将当前路径复制到对应集合。

为什么必须复制,而不能直接令 pathToP = tempPath?因为 tempPath 会在后续回溯中不断删除节点。如果两个变量指向同一个集合,已经找到的路径也会随之改变。

处理完当前节点的左右子树后,需要删除 tempPath 的最后一个节点:

复制代码
tempPath.remove(tempPath.size() - 1);

这就是回溯。例如从节点 6 返回节点 5 时,要把 6 移出当前路径,之后才能继续搜索节点 2。可以记成一句话:

进入节点时加入路径,离开节点时撤销选择。

pathToPpathToQ 都不为空时,说明两条路径已经找到,不必再继续搜索其他分支。

四、完整 Java 代码

复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root,
                                         TreeNode p,
                                         TreeNode q) {
        // DFS 搜索过程中的当前路径
        List<TreeNode> tempPath = new ArrayList<>();
        // 根节点到 p、q 的路径
        List<TreeNode> pathToP = new ArrayList<>();
        List<TreeNode> pathToQ = new ArrayList<>();

        dfs(root, p, q, tempPath, pathToP, pathToQ);

        TreeNode result = null;

        // 从根节点开始比较,最后一个相同节点就是最近公共祖先
        for (int i = 0; i < pathToP.size() && i < pathToQ.size(); i++) {
            if (pathToP.get(i) == pathToQ.get(i)) {
                result = pathToP.get(i);
            } else {
                break;
            }
        }

        return result;
    }

    private void dfs(TreeNode node,
                     TreeNode p,
                     TreeNode q,
                     List<TreeNode> tempPath,
                     List<TreeNode> pathToP,
                     List<TreeNode> pathToQ) {
        if (node == null) {
            return;
        }

        // 进入当前节点
        tempPath.add(node);

        if (node == p) {
            pathToP.addAll(tempPath);
        }

        if (node == q) {
            pathToQ.addAll(tempPath);
        }

        // 只要还有一条路径未找到,就继续搜索
        if (pathToP.isEmpty() || pathToQ.isEmpty()) {
            dfs(node.left, p, q, tempPath, pathToP, pathToQ);
            dfs(node.right, p, q, tempPath, pathToP, pathToQ);
        }

        // 离开当前节点,恢复进入前的路径状态
        tempPath.remove(tempPath.size() - 1);
    }
}

题目已经保证 pq 都存在于二叉树中,因此正常情况下两条路径一定能够找到。

五、关键代码解析

1. 为什么比较节点要使用 ==

复制代码
pathToP.get(i) == pathToQ.get(i)

路径中保存的是 TreeNode 对象。题目要求判断是否为树中的同一个节点,因此直接比较对象引用,而不是只比较 val。即使题目通常保证节点值唯一,比较节点本身也更准确。

2. 为什么能处理"节点是自己的祖先"

假设 p = 5q = 4

复制代码
pathToP:3 -> 5
pathToQ:3 -> 5 -> 2 -> 4

遍历到较短路径末尾时,最后一个相同节点是 5,因此算法自然返回 5,符合题目定义。

3. 提前停止条件为什么使用 ||

复制代码
if (pathToP.isEmpty() || pathToQ.isEmpty())

只要其中一条路径还没有找到,就仍需继续 DFS。只有两条路径都已经找到时,条件才为 false,搜索才会停止。如果误写成 &&,找到其中一个节点后就可能过早停止。

六、复杂度分析

设二叉树共有 n 个节点,高度为 h

  • 时间复杂度:O(n)。最坏情况下需要遍历整棵树,之后比较两条路径最多再花费 O(h)

  • 空间复杂度:O(h)。递归调用栈、临时路径及两条结果路径都与树高有关;最坏的链状树中为 O(n)

这里的复杂度分析不把保存答案路径所需空间忽略,因为它正是该解法的重要组成部分。

七、常见错误

  1. 找到目标后直接保存 tempPath 的引用,导致回溯时结果路径被修改;

  2. 递归返回前忘记删除当前节点,使不同分支的节点混入同一条路径;

  3. 使用节点值比较路径,而不是判断是否为同一个节点;

  4. 提前停止条件误写成两条路径都为空,导致只找到一个目标就停止;

  5. 从路径尾部反向查找公共节点。两条路径长度不同,直接从尾部比较并不能保证对齐,应该从共同的根节点向下比较。

八、总结

这道题的路径记录法非常直观:

复制代码
DFS 记录 root 到 p 的路径
        +
DFS 记录 root 到 q 的路径
        ↓
从头比较两条路径
        ↓
最后一个相同节点就是最近公共祖先

实现时最关键的是理解 DFS 回溯:tempPath 表示当前递归路径,进入节点时加入,退出节点时删除;找到目标后必须复制路径。掌握这个过程后,这种方法还可以迁移到"输出根到目标节点路径""判断两节点关系"等二叉树问题中。

相关推荐
码完就睡1 小时前
数据结构——树、二叉树基础概念
数据结构·算法
VL——MOESR2 小时前
【LuoguP1967】货车运输【生成树】【倍增】
c++·算法·题解·倍增·生成树
手写码匠2 小时前
华为云Flexus+DeepSeek征文|Agent 记忆系统实战:用 DeepSeek-R1/V3 + Dify 会话变量打造跨会话长期记忆
人工智能·深度学习·算法·aigc
Olafur_zbj2 小时前
【AI】CUDA编程中的维度
人工智能·算法
坚持编程的菜鸟3 小时前
模拟实现memcpy
c语言·算法·模拟实现my_memcpy
wabs6663 小时前
关于图论【最短路径之Bellman_ford 算法|卡码网94.城市间货物运输的思考】
数据结构·算法·图论·卡码网·bellman_ford·求最短路径
MrZhao4003 小时前
On-Policy Distillation(OPD):为什么大模型后训练要在学生自己的轨迹上蒸馏?
算法
朱峥嵘(朱髯)3 小时前
数据库如何根据全表 NDV 估算子集的 NDV
数据库·算法
jjjava2.04 小时前
牛客算法题(第四期)
算法
雪碧聊技术4 小时前
力扣 回溯法 | LCR 020. 回文子串
javascript·算法·leetcode