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
相关推荐
重生之我是Java开发战士42 分钟前
【动态规划】简单多状态dp问题:按摩师,打家劫舍,删除并获得点数,粉刷房子,买卖股票的最佳时机
算法·动态规划·哈希算法
KAU的云实验台1 小时前
单/多UAV、静/动态路径规划,基于PlatEMO平台的带约束多目标优化 本文核心内容:
算法·matlab·无人机
Liangwei Lin2 小时前
洛谷 P1807 最长路
数据结构·算法
会编程的土豆2 小时前
【数据结构与算法】二叉树从建立开始
数据结构·c++·算法
_日拱一卒2 小时前
LeetCode:最大子数组和
数据结构·算法·leetcode
计算机安禾2 小时前
【数据结构与算法】第22篇:线索二叉树(Threaded Binary Tree)
c语言·开发语言·数据结构·学习·算法·链表·visual studio code
算法鑫探2 小时前
解密2025数字密码:数位统计之谜
c语言·数据结构·算法·新人首发
计算机安禾3 小时前
【数据结构与算法】第21篇:二叉树遍历的经典问题:由遍历序列重构二叉树
c语言·数据结构·学习·算法·重构·visual studio code·visual studio
信奥胡老师3 小时前
P1255 数楼梯
开发语言·数据结构·c++·学习·算法
爱睡懒觉的焦糖玛奇朵4 小时前
【工业级落地算法之人员摔倒检测算法详解】
人工智能·python·深度学习·神经网络·算法·yolo·目标检测