【力扣100题】39.二叉树的最近公共祖先

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"

示例 1:

复制代码
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3

树结构:
        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4

节点 5 和节点 1 的最近公共祖先是节点 3

示例 2:

复制代码
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5

节点 5 和节点 4 的最近公共祖先是节点 5
因为节点 5 是节点 4 的祖先,且深度最大

示例 3:

复制代码
输入:root = [1,2], p = 1, q = 2
输出:1

节点 1 和节点 2 的最近公共祖先是节点 1
一个节点也可以是它自己的祖先

提示:

  • 树中节点数目在范围 [2, 10^5] 内
  • -10^9 <= Node.val <= 10^9
  • 所有 Node.val 互不相同
  • p != q
  • p 和 q 均存在于给定的二叉树中

解题思路总览

方法 思路 时间复杂度 空间复杂度 适用场景
方法一:后序遍历 递归后序遍历,左右子树查找,根据返回判断 O(n) O(h) 面试首选
方法二:存储父节点 用 HashMap 存储每个节点的父节点,然后上浮 O(n) O(n) 容易理解

核心原理: 如果一个节点既能在左子树找到 p/q,又能在右子树找到 p/q,那它就是最近公共祖先


方法一:后序遍历(推荐)

思路

利用递归后序遍历的思想,从底向上查找:

  1. 如果当前节点为空,或者等于 p 或 q,直接返回当前节点
  2. 递归在左子树和右子树中查找 p 和 q
  3. 根据左右子树的返回结果判断:
    • 左子树为空:说明 p/q 都在右子树,返回右子树结果
    • 右子树为空:说明 p/q 都在左子树,返回左子树结果
    • 都不为空:说明 p/q 分别在左右子树,当前节点就是最近公共祖先
    • 都为空:说明当前子树不包含 p/q,返回空

完整代码

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 递归终止条件:节点为空或找到 p 或 q
        if (root == NULL) return root;
        if (root == p || root == q) return root;

        // 在左右子树中查找
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);

        // 根据左右子树的返回结果判断
        if (left == NULL) return right;   // 左子树为空,p/q 都在右子树
        if (right == NULL) return left;  // 右子树为空,p/q 都在左子树
        return root;                      // 左右都不为空,root 是最近公共祖先
    }
};

算法流程图

以示例 1 为例,root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1

复制代码
树结构:
        3
       /    \
    5        1
    / \      / \
   6   2    0   8
       / \
      7   4

递归返回过程(后序遍历):

LCA(node=3, p=5, q=1):
  node==p? 否, node==q? 否
  left = LCA(node=5, p=5, q=1)
  right = LCA(node=1, p=5, q=1)

  LCA(node=5, p=5, q=1):
    node==p? 是 → return 5
  left = 5

  LCA(node=1, p=5, q=1):
    node==q? 是 → return 1
  right = 1

  left!=NULL && right!=NULL → return node(3)

返回 3

逐行解析

cpp 复制代码
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    // 递归终止条件:节点为空或找到 p 或 q
    if (root == NULL) return root;
    if (root == p || root == q) return root;
  • 递归终止条件一root == NULL,空树直接返回空。
  • 递归终止条件二root == p || root == q,找到了 p 或 q,返回当前节点。
cpp 复制代码
    TreeNode* left = lowestCommonAncestor(root->left, p, q);
    TreeNode* right = lowestCommonAncestor(root->right, p, q);
  • 递归在左子树和右子树中查找 p 和 q。
  • 使用后序遍历(左右根),确保从底向上返回结果。
cpp 复制代码
    if (left == NULL) return right;
  • 左子树为空,说明左子树中不存在 p 也不存在 q(否则已经返回了)。
  • p 和 q 一定都在右子树中,返回右子树的结果。
cpp 复制代码
    if (right == NULL) return left;
  • 右子树为空,说明 p 和 q 都在左子树中。
  • 返回左子树的结果。
cpp 复制代码
    return root;
  • 左右子树都不为空,说明 p 和 q 分别在左右子树中。
  • 当前节点就是它们的最近公共祖先。

复杂度分析

复杂度 说明
时间 O(n) 最坏情况遍历所有节点(当 p 和 q 在树的底部时)
空间 O(h) 递归栈深度,h 为树高

优点: 一次遍历搞定,时间效率高,代码简洁
缺点: 需要理解后序遍历和递归返回值的设计


方法二:存储父节点

思路

  1. 第一次 DFS,存储每个节点的父节点
  2. 将 p 的所有祖先节点(包括 p 自己)加入集合
  3. 从 q 开始向上浮,一直找到第一个出现在集合中的节点

完整代码

cpp 复制代码
class Solution {
public:
    unordered_map<TreeNode*, TreeNode*> parent;

    void dfs(TreeNode* node, TreeNode* par) {
        if (!node) return;
        parent[node] = par;
        dfs(node->left, node);
        dfs(node->right, node);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        dfs(root, NULL);  // 记录每个节点的父节点

        unordered_set<TreeNode*> ancestors;
        // 将 p 及其所有祖先加入集合
        while (p) {
            ancestors.insert(p);
            p = parent[p];
        }

        // 从 q 开始向上找,第一个在集合中的就是 LCA
        while (q) {
            if (ancestors.count(q)) {
                return q;
            }
            q = parent[q];
        }

        return NULL;  // 不会走到这里
    }
};

复杂度分析

复杂度 说明
时间 O(n) 两次遍历
空间 O(n) HashMap 和 HashSet 存储节点

两种方法对比

维度 方法一 后序遍历 方法二 存储父节点
代码复杂度 简洁 中等
时间复杂度 O(n) O(n)
空间复杂度 O(h) O(n)
面试推荐度 首选 容易想到
特点 一次遍历搞定 需要额外的 parent HashMap

面试追问 FAQ

问题 解答
Q1:为什么后序遍历能找到最近公共祖先? 后序遍历的特点是从叶子节点向上返回。递归返回时,如果一个节点的左右子树分别包含 p 和 q,那该节点就是最近公共祖先(因为它既是 p 的祖先也是 q 的祖先,且深度最大)。
Q2:为什么 `root == p
Q3:如何理解 if (left == NULL) return right 左子树为空有两种情况:1)左子树不包含 p 也不包含 q;2)左子树包含了 p 或 q 但没有返回(这种情况不存在,因为找到就会返回)。所以如果左子树为空,p 和 q 一定都在右子树中。
Q4:如果 p 是 q 的祖先,结果是什么? 结果是 p。当递归到节点 p 时,root == p 直接返回 p。然后 p 作为返回值一路向上传递,最终成为最终结果。
Q5:方法二为什么需要 parent HashMap? 因为方法二是用"上浮"的方式查找。从 q 开始,依次访问父节点,直到找到第一个也在 p 的祖先链上的节点。这个"父节点"关系需要用 HashMap 存储。
Q6:两种方法如何选择? 方法一更优,因为只需要 O(h) 空间且一次遍历搞定。方法二适合需要频繁查询两个节点的 LCA 的场景(可以预处理 parent 信息)。

相关题目

题目 难度 关键点
236. 二叉树的最近公共祖先 中等 后序遍历,LCA
235. 二叉搜索树的最近公共祖先 简单 利用 BST 性质
剑指 Offer 68-II. 二叉树的最近公共祖先 简单 同 236
1644. 二叉树中第 K 小的元素 中等 BST + LCA 变形

总结

要点 说明
核心原理 后序遍历从底向上,如果左右子树分别包含 p 和 q,当前节点就是 LCA
递归终止条件 root == NULL 或 root == p 或 root==q
返回值判断 left == NULL 返回 right,right==NULL 返回 left,都不空返回 root
时间复杂度 O(n),每个节点最多访问一次
空间复杂度 O(h),递归栈深度

相关推荐
逻辑驱动的ken1 小时前
Java高频面试考点场景题30
java·开发语言·深度学习·面试·职场和发展
无敌昊哥战神1 小时前
【LeetCode 134】加油站:图解指针跳跃与 O(N) 极简贪心,避开 Python 隐藏坑!
c语言·python·算法·leetcode
人道领域1 小时前
【LeetCode刷题日记】222.极速计算完全二叉树节点数:O(log²n)算法揭秘
java·数据结构·算法·leetcode·深度优先
目黑live +wacyltd1 小时前
算法备案的实操指南(含截图示例)
人工智能·算法·llm·大模型备案·算法备案
小糯米6011 小时前
C语言 指针4
c语言·数据结构·算法
略知java的景初1 小时前
【面试特集】JVM 内存与对象
jvm·面试·职场和发展
洛水水1 小时前
【力扣100题】36.二叉树展开为链表
算法·leetcode·链表
lwf0061641 小时前
PNN (Product-based Neural Network) 学习日记
算法·机器学习
ZPC82101 小时前
YOLO-3D + 双目相机 (RGB + 深度 + 点云) → 3D 位置 + 抓取姿态
人工智能·算法·计算机视觉·机器人