【二叉搜索树最近公共祖先】迭代法VS递归法解决

目录

一、什么是最近公共祖先(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,并对比两种方法的优劣。


一、什么是最近公共祖先(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]

【二叉搜索树最近公共祖先】迭代法VS递归法解决_第1张图片

示例 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, 因为根据定义最近公共祖先节点可以为节点本身。

二、迭代法求解 BST 的 LCA

1、思路讲解

        做过二叉树公共祖先问题的同学应该知道,利用回溯从底向上搜索,若一个节点的左子树里含p,右子树里含q,则该节点是最近公共祖先。现在是二叉搜索树,可以利用其有序性:如果中间节点是 q 和 p 的公共祖先,其数值必定在 [p, q]闭区间中。那只要从上到下遍历,若遇到节点的数值在[p, q]闭区间中,则说明该节点就是p 和 q的公共祖先。
        迭代法通过循环模拟递归过程,避免了递归的函数调用开销。

关键步骤:

  1. 从根节点开始遍历。
  2. 如果当前节点的值大于 p 和 q 的值,则向左子树移动。
  3. 如果当前节点的值小于 p 和 q 的值,则向右子树移动。
  4. 否则,当前节点就是 LCA。

2、关键函数讲解

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 表示未找到。

3、完整代码

#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

1、思路讲解

        在 BST 中,节点的值具有有序性(左子树所有节点值 < 根节点值 < 右子树所有节点值)。基于这一特性,递归法求解 LCA 的思路如下:

  1. 比较当前节点值与 p 和 q 的值
    • 如果当前节点的值 大于 p 和 q 的值,则 LCA 一定在左子树中。
    • 如果当前节点的值 小于 p 和 q 的值,则 LCA 一定在右子树中。
    • 如果当前节点的值 介于 p 和 q 的值之间(或等于其中一个节点值),则当前节点就是 LCA。
  2. 递归终止条件
    • 当遍历到叶子节点仍未找到 LCA 时,返回 nullptr(但 BST 的 LCA 一定存在,因此这种情况理论上不会发生)。

2、关键函数

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;
    }
}

3.完整代码

#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;
}

四.递归法 vs 迭代法

时间复杂度:迭代法与递归法相同,为 O(h)。

迭代法通常不需要额外的栈空间,因此在空间使用上更高效,有些情况递归的深度受到限制,可能导致栈溢出错误。迭代法则避免了此问题。

方法 优点 缺点 适用场景
递归法 代码简洁,易于理解 可能有栈溢出风险(对于极深树) 树深度较小或对代码可读性要求高的场景
迭代法 空间复杂度低,无栈溢出风险 代码稍复杂,需要手动维护循环 树深度较大或对性能要求高的场景

五.LCA 的实际应用

1. 数据库索引

场景描述
在数据库系统中,索引用于加速数据检索。例如,B+树是一种常用的索引结构,它通过多级节点组织数据,类似于二叉搜索树(BST)的扩展。

LCA 的作用

  • 范围查询优化
    在 B+树中,LCA 可以用于快速定位范围查询的起始和结束位置。例如,查询 [a, b] 范围内的数据时,可以找到 a 和 b 的 LCA,然后从该节点开始遍历子树,获取所有符合条件的记录。
  • 合并查询结果
    当需要合并多个查询条件的结果时,LCA 可以帮助确定共享的祖先节点,从而减少重复计算。

示例
假设有一个 B+树索引,存储了用户 ID 的范围。查询用户 ID 在 [10, 20] 范围内的数据:

  1. 找到 10 和 20 的 LCA 节点。
  2. 从该节点开始遍历子树,收集所有 10 ≤ ID ≤ 20 的记录。

2. 路径规划

场景描述
在地图导航或网络路由中,路径规划需要找到从起点到终点的最短路径。LCA 可以用于优化路径计算,尤其是在层次化网络结构中(如交通网络、通信网络)。

LCA 的作用

  • 共享路径计算
    如果起点和终点有共同的祖先节点(LCA),则从起点到 LCA 和从 LCA 到终点的路径可以合并为一条完整路径。
  • 减少计算量
    通过找到 LCA,可以避免重复计算共享的路径部分,从而提高算法效率。

示例
在一个层次化的交通网络中(如城市→区域→街道),从 A 地到 B 地的路径规划:

  1. 找到 A 和 B 的 LCA(例如,某个区域节点)。
  2. 计算从 A 到 LCA 的路径,以及从 LCA 到 B 的路径。
  3. 合并两条路径,得到完整路径。

3. 社交网络分析

场景描述
在社交网络中,用户之间的关系可以表示为图结构。LCA 可以用于分析用户之间的关联性,例如查找共同好友、共同兴趣组或共同群组。

LCA 的作用

  • 共同好友查找
    将社交网络建模为树结构(例如,基于用户关系的层次化树),找到两个用户的 LCA,可以确定他们的共同好友或共同社交圈。
  • 兴趣组推荐
    通过分析用户的兴趣树(例如,兴趣标签的层次结构),找到两个用户兴趣的 LCA,可以推荐相关的兴趣组或活动。
  • 社区发现
    LCA 可以用于识别紧密连接的社区或子群,帮助分析社交网络的拓扑结构。

4.LCA 在其他领域的应用

  1. 版本控制系统
    • 在 Git 等版本控制系统中,LCA 可以用于确定两个提交的共同祖先,从而计算差异或合并分支。
  2. 生物信息学
    • 在系统发育树中,LCA 可以用于分析物种的进化关系,例如查找两个物种的最近共同祖先。
  3. XML 查询处理
    • 在 XML 文档中,LCA 可以用于快速定位两个节点的共同祖先,从而优化 XPath 查询。


总结

通过本文的讲解,我们深入理解了 BST 中 LCA 问题的递归法和迭代法实现。递归法简洁直观,适合教学和小规模数据;迭代法高效稳定,适合大规模数据或对性能敏感的场景。在实际应用中,可以根据具体需求选择合适的方法,并进一步优化以适应更复杂的场景。

你可能感兴趣的:(c++,二叉搜索树,最近公共祖先,递归和迭代)