二叉树的最近公共祖先(LCA)问题详解
问题描述
给定一个二叉树,找到该树中两个指定节点的最近公共祖先(Lowest Common Ancestor, LCA)。
最近公共祖先的定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
解题思路
核心思想
本解法采用两遍遍历的策略:
- 第一遍(DFS):统计每个节点子树中包含目标节点(p 或 q)的个数
- 第二遍(Helper):根据统计结果,找到同时包含两个目标节点的最小子树根节点
算法步骤
步骤 1:统计子树中包含目标节点的个数
使用 dfs 函数递归遍历整棵树,对于每个节点 root:
- 如果
root本身就是 p 或 q,计数 +1 - 递归统计左子树和右子树中包含目标节点的个数
- 将总数存入
hasCount[root]
关键点 :hasCount[root] 表示以 root 为根的子树中包含 p 或 q 的节点总数(0、1 或 2)。
步骤 2:查找最近公共祖先
使用 helper 函数根据统计结果查找 LCA:
LCA 的判断条件:
-
情况一:当前节点是 p 或 q,且其左子树或右子树包含另一个目标节点
cppif(root == p || root == q){ if(hasCount[root->left]==1 || hasCount[root->right]==1){ return root; // 当前节点就是 LCA } } -
情况二:当前节点的左子树和右子树各包含一个目标节点
cppif(hasCount[root->left] == 1 && hasCount[root->right] == 1){ return root; // 当前节点就是 LCA } -
情况三:如果当前节点不是 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 查找:
- 检查节点 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)
算法优势
- 思路清晰:两遍遍历,先统计后查找,逻辑直观
- 易于理解:通过统计信息判断 LCA,避免了复杂的回溯逻辑
- 通用性强:适用于任意二叉树结构
算法优化建议
虽然当前解法已经能正确解决问题,但可以考虑以下优化:
优化版本(单遍遍历)
实际上,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 问题的方法:
- 第一遍 DFS 统计每个节点子树中包含目标节点的个数
- 第二遍根据统计结果查找最近公共祖先
虽然这种方法不是最优解,但它思路清晰、易于理解,适合初学者掌握 LCA 问题的本质。在实际面试中,建议使用单遍遍历的优化版本,既高效又简洁。
相关题目:
- LeetCode 236: 二叉树的最近公共祖先
- LeetCode 235: 二叉搜索树的最近公共祖先
- LeetCode 1644: 二叉树的最近公共祖先 II