LeetCode236-二叉树的最近公共祖先(LCA)问题详解-C++

二叉树的最近公共祖先(LCA)问题详解

问题描述

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

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

解题思路

核心思想

本解法采用两遍遍历的策略:

  1. 第一遍(DFS):统计每个节点子树中包含目标节点(p 或 q)的个数
  2. 第二遍(Helper):根据统计结果,找到同时包含两个目标节点的最小子树根节点

算法步骤

步骤 1:统计子树中包含目标节点的个数

使用 dfs 函数递归遍历整棵树,对于每个节点 root

  • 如果 root 本身就是 p 或 q,计数 +1
  • 递归统计左子树和右子树中包含目标节点的个数
  • 将总数存入 hasCount[root]

关键点hasCount[root] 表示以 root 为根的子树中包含 p 或 q 的节点总数(0、1 或 2)。

步骤 2:查找最近公共祖先

使用 helper 函数根据统计结果查找 LCA:

LCA 的判断条件

  1. 情况一:当前节点是 p 或 q,且其左子树或右子树包含另一个目标节点

    cpp 复制代码
    if(root == p || root == q){
        if(hasCount[root->left]==1 || hasCount[root->right]==1){
            return root;  // 当前节点就是 LCA
        }
    }
  2. 情况二:当前节点的左子树和右子树各包含一个目标节点

    cpp 复制代码
    if(hasCount[root->left] == 1 && hasCount[root->right] == 1){
        return root;  // 当前节点就是 LCA
    }
  3. 情况三:如果当前节点不是 LCA,递归查找左子树或右子树

完整代码实现

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:
    // 哈希表:记录每个节点子树中包含 p 或 q 的个数
    std::unordered_map<TreeNode*, int> hasCount;
    
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 第一遍:统计每个节点子树中包含目标节点的个数
        dfs(root, p, q);
        // 第二遍:根据统计结果查找 LCA
        return helper(root, p, q);
    }
    
    // 递归查找 LCA
    TreeNode* helper(TreeNode* root, TreeNode* p, TreeNode* q){
        if(root == nullptr){
            return root;
        }
        
        // 情况一:当前节点是目标节点,且子树包含另一个目标节点
        if(root == p || root == q){
            if(hasCount[root->left]==1 || hasCount[root->right]==1){
                return root;
            }
        }
        
        // 情况二:左右子树各包含一个目标节点
        if(hasCount[root->left] == 1 && hasCount[root->right] == 1){
            return root;
        }
        
        // 情况三:递归查找左子树
        TreeNode* res = helper(root->left, p, q);
        if(res != nullptr){
            return res;
        }
        // 递归查找右子树
        return helper(root->right, p, q);
    }

    // DFS 统计每个节点子树中包含目标节点的个数
    int dfs(TreeNode* root, TreeNode* p, TreeNode* q){
        if (root == nullptr){
            return 0;
        }

        int count = 0;
        // 如果当前节点是目标节点,计数 +1
        if(root == p || root == q){
            count += 1;
        }

        // 递归统计左右子树
        count += ( dfs(root->left, p, q) + dfs(root->right, p, q) );
        
        // 记录当前节点的统计结果
        hasCount[root] = count;
        return count;
    }
};

算法示例

假设有以下二叉树:

复制代码
        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4

查找节点 5 和节点 1 的最近公共祖先。

执行过程

第一遍 DFS 统计:
复制代码
节点 3: hasCount[3] = 2  (包含 5 和 1)
节点 5: hasCount[5] = 1  (包含 5)
节点 1: hasCount[1] = 1  (包含 1)
节点 2: hasCount[2] = 0  (不包含目标节点)
...
第二遍 Helper 查找:
  1. 检查节点 3:
    • hasCount[3->left] = hasCount[5] = 1
    • hasCount[3->right] = hasCount[1] = 1
    • 满足情况二:左右子树各包含一个目标节点
    • 返回节点 3(LCA)

复杂度分析

  • 时间复杂度:O(n)

    • DFS 遍历整棵树一次:O(n)
    • Helper 遍历整棵树一次:O(n)
    • 总时间复杂度:O(n)
  • 空间复杂度:O(n)

    • 递归栈空间:O(h),h 为树的高度,最坏情况 O(n)
    • 哈希表空间:O(n)
    • 总空间复杂度:O(n)

算法优势

  1. 思路清晰:两遍遍历,先统计后查找,逻辑直观
  2. 易于理解:通过统计信息判断 LCA,避免了复杂的回溯逻辑
  3. 通用性强:适用于任意二叉树结构

算法优化建议

虽然当前解法已经能正确解决问题,但可以考虑以下优化:

优化版本(单遍遍历)

实际上,LCA 问题可以用单遍遍历解决,无需预先统计:

cpp 复制代码
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if (root == nullptr || root == p || root == q) {
        return root;
    }
    
    TreeNode* left = lowestCommonAncestor(root->left, p, q);
    TreeNode* right = lowestCommonAncestor(root->right, p, q);
    
    // 如果左右子树都找到了目标节点,当前节点就是 LCA
    if (left != nullptr && right != nullptr) {
        return root;
    }
    
    // 否则返回非空的那一边
    return left != nullptr ? left : right;
}

这个优化版本:

  • 时间复杂度:O(n),只需遍历一次
  • 空间复杂度:O(h),只需递归栈空间
  • 代码更简洁:无需额外的哈希表

总结

本文介绍了一种通过两遍遍历解决 LCA 问题的方法:

  1. 第一遍 DFS 统计每个节点子树中包含目标节点的个数
  2. 第二遍根据统计结果查找最近公共祖先

虽然这种方法不是最优解,但它思路清晰、易于理解,适合初学者掌握 LCA 问题的本质。在实际面试中,建议使用单遍遍历的优化版本,既高效又简洁。


相关题目

  • LeetCode 236: 二叉树的最近公共祖先
  • LeetCode 235: 二叉搜索树的最近公共祖先
  • LeetCode 1644: 二叉树的最近公共祖先 II
相关推荐
触底反弹8 分钟前
一文彻底搞懂 JavaScript 栈和队列(建议收藏)
javascript·算法·面试
WL学习笔记13 分钟前
通讯录(顺序表实现)
c语言·数据结构·算法
Jerryhut33 分钟前
opencv对齐算法及其应用
人工智能·opencv·算法
果丁智能44 分钟前
智慧校园一卡通深度融合方案:基于超级SIM卡的手机碰一碰智能开锁技术落地实践
数据结构·人工智能·python·科技·算法·智能家居·信息与通信
满怀冰雪1 小时前
第13篇-栈算法入门-括号匹配-表达式与单调栈基础
java·算法
TCW11211 小时前
AI底层系列:用C++实现线性代数的公式推导与算法设计-基础篇-5.矩阵方程
人工智能·线性代数·算法
叫我:松哥1 小时前
基于机器学习和flask的体育健身风险智能分析系统,系统集成DeepSeek、聚类算法、分类算法等,准确率达90%
人工智能·python·神经网络·算法·机器学习·flask·聚类
wabs6661 小时前
关于动态规划【0-1背包思想在实际问题中是怎么转化的?】
算法·动态规划
阿文的代码库1 小时前
欧拉回路与欧拉路径的算法流程演示
算法
汤姆yu1 小时前
云知声 U2 原生智能体大模型深度解析
大数据·人工智能·算法·ai·大模型·多模态·智能体