一、什么是最近公共祖先(LCA)?
二、迭代法求解 BST 的 LCA
1、思路讲解:
2、关键函数讲解
3、完整代码
三.递归法求解 BST 的 LCA
1、思路讲解
2、关键函数
3.完整代码
四.递归法 vs 迭代法
五.LCA 的实际应用
1. 数据库索引
2. 路径规划
3. 社交网络分析
4.LCA 在其他领域的应用
总结
前言
在二叉搜索树(BST)中,最近公共祖先(Lowest Common Ancestor,LCA)是一个关键问题,广泛应用于路径规划、社交网络分析、文件系统管理等场景。本文将通过迭代法和递归法两种方式,详细讲解如何在 BST 中高效求解 LCA,并对比两种方法的优劣。
在二叉树中,两个节点 p
和 q
的最近公共祖先是指同时包含 p
和 q
的最深节点。对于 BST,由于节点值具有有序性,我们可以利用这一特性快速定位 LCA。
示例(leetcode235):
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 输出: 6 解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 输出: 2 解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
做过二叉树公共祖先问题的同学应该知道,利用回溯从底向上搜索,若一个节点的左子树里含p,右子树里含q,则该节点是最近公共祖先。现在是二叉搜索树,可以利用其有序性:如果中间节点是 q 和 p 的公共祖先,其数值必定在 [p, q]闭区间中。那只要从上到下遍历,若遇到节点的数值在[p, q]闭区间中,则说明该节点就是p 和 q的公共祖先。
迭代法通过循环模拟递归过程,避免了递归的函数调用开销。
关键步骤:
p
和 q
的值,则向左子树移动。p
和 q
的值,则向右子树移动。TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
TreeNode* current = root;
while (current) {
if (current->val > p->val && current->val > q->val) {
current = current->left;
} else if (current->val < p->val && current->val < q->val) {
current = current->right;
} else {
return current;
}
}
return nullptr;
}
lowestCommonAncestor函树中使用迭代方法遍历树,首先定义一个指针 current,初始化为根节点 root,用于遍历。开始一个 while 循环,遍历到叶子节点为止。如果current 节点的值大于 p 和 q 的值,说明 p 和q 都在当前节点的左子树中,需要将 current 移动到其左子节点,继续在左子树中查找;小于则继续在右子树中查找。如果current节点的值位于p和q的闭区间,则current节点是最近公共祖先。如果遍历结束仍未找到公共祖先,返回 nullptr 表示未找到。
#include
// 定义二叉树节点结构体
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 函数用于找到两个节点的最近公共祖先
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 初始化当前节点为根节点
TreeNode* current = root;
// 迭代遍历树
while (current) {
// 如果当前节点的值大于p和q的值,移动到左子节点
if (current->val > p->val && current->val > q->val) {
current = current->left;
}
// 如果当前节点的值小于p和q的值,移动到右子节点
else if (current->val < p->val && current->val < q->val) {
current = current->right;
}
// 否则,当前节点就是最近公共祖先
else {
return current;
}
}
return nullptr; // 如果没有找到,返回nullptr
}
// 插入节点到二叉搜索树
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr) {
TreeNode* new_node = new TreeNode(val);
return new_node;
}
if (root->val > val) {
root->left = insertIntoBST(root->left, val);
}
if (root->val < val) {
root->right = insertIntoBST(root->right, val);
}
return root;
}
// 在树中查找节点
TreeNode* findNode(TreeNode* root, int val) {
if (root == nullptr || root->val == val) {
return root;
}
if (root->val > val) {
return findNode(root->left, val);
}
return findNode(root->right, val);
}
int main() {
int n = 0;
TreeNode* root = nullptr;
std::cout << "请输入二叉搜索树的节点值(输入-1结束):\n";
while (n != -1) {
std::cin >> n;
if (n != -1) {
root = insertIntoBST(root, n);
}
}
int pVal, qVal;
std::cout << "请输入要查找公共祖先的两个节点的值:\n";
TreeNode* p, * q;
while (1) {
std::cin >> pVal >> qVal;
// 查找节点p和q
p = findNode(root, pVal);
q = findNode(root, qVal);
if (p && q) {
break;
}
else {
std::cout << "输入的节点值无效或不在树中,请重新输入:\n";
}
}
// 找到并打印最近公共祖先的值
TreeNode* ancestor = lowestCommonAncestor(root, p, q);
if (ancestor) {
std::cout << "节点 " << p->val << " 和节点 " << q->val << " 的最近公共祖先是节点 " << ancestor->val << ".\n";
}
else {
std::cout << "没有找到公共祖先!\n";
}
return 0;
}
在 BST 中,节点的值具有有序性(左子树所有节点值 < 根节点值 < 右子树所有节点值)。基于这一特性,递归法求解 LCA 的思路如下:
p
和 q
的值:
p
和 q
的值,则 LCA 一定在左子树中。p
和 q
的值,则 LCA 一定在右子树中。p
和 q
的值之间(或等于其中一个节点值),则当前节点就是 LCA。nullptr
(但 BST 的 LCA 一定存在,因此这种情况理论上不会发生)。TreeNode* lowestCommonAncestorRecursive(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return nullptr;
// 如果当前节点值大于 p 和 q 的值,LCA 在左子树
if (root->val > p->val && root->val > q->val) {
return lowestCommonAncestorRecursive(root->left, p, q);
}
// 如果当前节点值小于 p 和 q 的值,LCA 在右子树
else if (root->val < p->val && root->val < q->val) {
return lowestCommonAncestorRecursive(root->right, p, q);
}
// 否则,当前节点就是 LCA
else {
return root;
}
}
#include
// 定义二叉树节点结构体
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 函数用于找到两个节点的最近公共祖先
TreeNode* lowestCommonAncestorRecursive(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return nullptr;
// 如果当前节点值大于 p 和 q 的值,LCA 在左子树
if (root->val > p->val && root->val > q->val) {
return lowestCommonAncestorRecursive(root->left, p, q);
}
// 如果当前节点值小于 p 和 q 的值,LCA 在右子树
else if (root->val < p->val && root->val < q->val) {
return lowestCommonAncestorRecursive(root->right, p, q);
}
// 否则,当前节点就是 LCA
else {
return root;
}
}
// 插入节点到二叉搜索树
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr) {
TreeNode* new_node = new TreeNode(val);
return new_node;
}
if (root->val > val) {
root->left = insertIntoBST(root->left, val);
}
if (root->val < val) {
root->right = insertIntoBST(root->right, val);
}
return root;
}
// 在树中查找节点
TreeNode* findNode(TreeNode* root, int val) {
if (root == nullptr || root->val == val) {
return root;
}
if (root->val > val) {
return findNode(root->left, val);
}
return findNode(root->right, val);
}
int main() {
int n = 0;
TreeNode* root = nullptr;
std::cout << "请输入二叉搜索树的节点值(输入-1结束):\n";
while (n != -1) {
std::cin >> n;
if (n != -1) {
root = insertIntoBST(root, n);
}
}
int pVal, qVal;
std::cout << "请输入要查找公共祖先的两个节点的值:\n";
TreeNode* p, * q;
while (1) {
std::cin >> pVal >> qVal;
// 查找节点p和q
p = findNode(root, pVal);
q = findNode(root, qVal);
if (p && q) {
break;
}
else {
std::cout << "输入的节点值无效或不在树中,请重新输入:\n";
}
}
// 找到并打印最近公共祖先的值
TreeNode* ancestor = lowestCommonAncestor(root, p, q);
if (ancestor) {
std::cout << "节点 " << p->val << " 和节点 " << q->val << " 的最近公共祖先是节点 " << ancestor->val << ".\n";
}
else {
std::cout << "没有找到公共祖先!\n";
}
return 0;
}
时间复杂度:迭代法与递归法相同,为 O(h)。
迭代法通常不需要额外的栈空间,因此在空间使用上更高效,有些情况递归的深度受到限制,可能导致栈溢出错误。迭代法则避免了此问题。
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
递归法 | 代码简洁,易于理解 | 可能有栈溢出风险(对于极深树) | 树深度较小或对代码可读性要求高的场景 |
迭代法 | 空间复杂度低,无栈溢出风险 | 代码稍复杂,需要手动维护循环 | 树深度较大或对性能要求高的场景 |
场景描述:
在数据库系统中,索引用于加速数据检索。例如,B+树是一种常用的索引结构,它通过多级节点组织数据,类似于二叉搜索树(BST)的扩展。
LCA 的作用:
[a, b]
范围内的数据时,可以找到 a
和 b
的 LCA,然后从该节点开始遍历子树,获取所有符合条件的记录。示例:
假设有一个 B+树索引,存储了用户 ID 的范围。查询用户 ID 在 [10, 20]
范围内的数据:
10
和 20
的 LCA 节点。10 ≤ ID ≤ 20
的记录。场景描述:
在地图导航或网络路由中,路径规划需要找到从起点到终点的最短路径。LCA 可以用于优化路径计算,尤其是在层次化网络结构中(如交通网络、通信网络)。
LCA 的作用:
示例:
在一个层次化的交通网络中(如城市→区域→街道),从 A
地到 B
地的路径规划:
A
和 B
的 LCA(例如,某个区域节点)。A
到 LCA 的路径,以及从 LCA 到 B
的路径。场景描述:
在社交网络中,用户之间的关系可以表示为图结构。LCA 可以用于分析用户之间的关联性,例如查找共同好友、共同兴趣组或共同群组。
LCA 的作用:
通过本文的讲解,我们深入理解了 BST 中 LCA 问题的递归法和迭代法实现。递归法简洁直观,适合教学和小规模数据;迭代法高效稳定,适合大规模数据或对性能敏感的场景。在实际应用中,可以根据具体需求选择合适的方法,并进一步优化以适应更复杂的场景。