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
相关推荐
啊阿狸不会拉杆2 小时前
《数字图像处理》第 7 章 - 小波与多分辨率处理
图像处理·人工智能·算法·计算机视觉·数字图像处理
程序猿炎义3 小时前
【Easy-VectorDB】Faiss数据结构与索引类型
数据结构·算法·faiss
天赐学c语言3 小时前
1.20 - x的平方根 && vector的扩容机制以及删除元素是否会释放内存
c++·算法·leecode
52Hz1184 小时前
力扣24.两两交换链表中的节点、25.K个一组反转链表
算法·leetcode·链表
老鼠只爱大米4 小时前
LeetCode经典算法面试题 #160:相交链表(双指针法、长度差法等多种方法详细解析)
算法·leetcode·链表·双指针·相交链表·长度差法
ValhallaCoder4 小时前
Day53-图论
数据结构·python·算法·图论
老鼠只爱大米5 小时前
LeetCode经典算法面试题 #84:柱状图中最大的矩形(单调栈、分治法等四种方法详细解析)
算法·leetcode·动态规划·单调栈·分治法·柱状图最大矩形
C雨后彩虹5 小时前
羊、狼、农夫过河
java·数据结构·算法·华为·面试
重生之后端学习5 小时前
19. 删除链表的倒数第 N 个结点
java·数据结构·算法·leetcode·职场和发展