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
相关推荐
程序员清洒1 天前
CANN模型剪枝:从敏感度感知到硬件稀疏加速的全链路压缩实战
算法·机器学习·剪枝
vortex51 天前
几种 dump hash 方式对比分析
算法·哈希算法
Wei&Yan1 天前
数据结构——顺序表(静/动态代码实现)
数据结构·c++·算法·visual studio code
团子的二进制世界1 天前
G1垃圾收集器是如何工作的?
java·jvm·算法
吃杠碰小鸡1 天前
高中数学-数列-导数证明
前端·数学·算法
故事不长丨1 天前
C#线程同步:lock、Monitor、Mutex原理+用法+实战全解析
开发语言·算法·c#
long3161 天前
Aho-Corasick 模式搜索算法
java·数据结构·spring boot·后端·算法·排序算法
近津薪荼1 天前
dfs专题4——二叉树的深搜(验证二叉搜索树)
c++·学习·算法·深度优先
熊文豪1 天前
探索CANN ops-nn:高性能哈希算子技术解读
算法·哈希算法·cann
熊猫_豆豆1 天前
YOLOP车道检测
人工智能·python·算法