[算法系列之三十一]最近公共祖先(LCA)

简介:

【简介】

对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。

另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。

例如,对于下面的树,结点4和结点6的最近公共祖先LCA(T,4,6)为结点2。


求树中两个结点的最低公共祖先是面试中经常出现的一个问题。一般的做法,可能是针对是否为二叉查找树分情况讨论。

LCA问题的扩展主要在于结点是否只包含父结点指针,对于同一棵树是否进行多次LCA查询。下面分别进行说明。

【二叉查找树情况】

【创建二叉查找树】

// 二叉查找树节点
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

// 插入
void TreeInsert(TreeNode*& root,int val){
    // 创建新节点
    TreeNode* node = new TreeNode(val);
    // 空树
    if(root == NULL){
        root = node;
        return;
    }//if
    TreeNode *pre = NULL;
    TreeNode *p = root;
    // 寻找插入位置
    while(p){
        // 父节点
        pre = p;
        // 沿左子树方向下降
        if(val < p->val){
            p = p->left;
        }//if
        // 沿右子树方向下降
        else{
            p = p->right;
        }//else
    }//while
    // 左子结点处插入
    if(val < pre->val){
        pre->left = node;
    }//if
    // 右子结点处插入
    else{
        pre->right = node;
    }//else
}


【思路】
  • 如果当前结点t 大于结点u、v,说明u、v都在t 的左侧,所以它们的共同祖先必定在t 的左子树中,故从t 的左子树中继续查找;
  • 如果当前结点t 小于结点u、v,说明u、v都在t 的右侧,所以它们的共同祖先必定在t 的右子树中,故从t 的右子树中继续查找;
  • 如果当前结点t 满足 u <t < v,说明u和v分居在t 的两侧,故当前结点t 即为最近公共祖先;
  • 而如果u是v的祖先,那么返回u的父结点,同理,如果v是u的祖先,那么返回v的父结点。

【版本一递归版】

这个版本默认要查找节点u,v在root树中

// 最近公共祖先
TreeNode *LCA(TreeNode *root,TreeNode *u,TreeNode *v){
    // 空树
    if(root == nullptr || u == nullptr || v == nullptr){
        return nullptr;
    }//if
    // u < t < v  or v < t < u
    if((u->val < root->val && root->val < v->val) ||
       (v->val < root->val && root->val < u->val)){
        return root;
    }//if
    // u < root and v < root  left sub tree
    if(u->val < root->val && v->val < root->val){
        // u是v祖先 or v是u的祖先
        if(root->left == u || root->left == v){
            return root;
        }//if
        return LCA(root->left,u,v);
    }//if
    // u > root and v > root  right sub tree
    if(u->val > root->val && v->val > root->val){
        // u是v祖先 or v是u的祖先
        if(root->right == u || root->right == v){
            return root;
        }//if
        return LCA(root->right,u,v);
    }//if
    return nullptr;
}

【版本二迭代版】

/**--------------------------------
*   日期:2015-02-02
*   作者:SJF0115
*   题目: 最近公共祖先(LCA)迭代版
*   博客:
------------------------------------**/
TreeNode *LCA(TreeNode *root,TreeNode *u,TreeNode *v){
    // 空树
    if (root == nullptr || u == nullptr || v == nullptr) {
        return nullptr;
    }//if
    TreeNode *leftNode = u;
    TreeNode *rightNode = v;
    TreeNode *parentNode = nullptr;
    //二叉查找树内,如果左结点大于右结点,不对,交换
    if (leftNode->val > rightNode->val) {
        TreeNode *tmp = leftNode;
        leftNode = rightNode;
        rightNode = tmp;
    }//if
    while(root){
         // u < t < v  or v < t < u   在root两侧
        if(leftNode->val < root->val && root->val < rightNode->val){
            return root;
        }//if
        // u < root v < root left sub tree
        if(rightNode->val < root->val){
            parentNode = root;
            root = root->left;
        }//if
        // u > root v > root right sub tree
        else if(leftNode->val > root->val){
            parentNode = root;
            root = root->right;
        }
        // u是v祖先 or v是u的祖先
        else if(root == leftNode || root == rightNode){
            return parentNode;
        }
    }//while
    return nullptr;
}

【普通二叉树】

【版本一】
所谓共同的父结点,就是两个结点都出现在这个结点的子树中。因此我们可以定义一函数,来判断一个结点的子树中是不是包含了另外一个结点。
这不是件很难的事,我们可以用递归的方法来实现:
// root树中是否包含节点node
bool Exits(TreeNode *root,TreeNode *node){
    // find the node
    if(root == node){
        return true;
    }//if
    // left sub tree
    bool isExits = false;
    if(root->left){
        isExits = Exits(root->left,node);
    }//if
    // right sub tree
    if(!isExits && root->right){
        isExits = Exits(root->right,node);
    }//if
    return isExits;
}

我们可以从根结点开始,判断以当前结点为根的树中左右子树是不是包含我们要找的两个结点。如果两个结点都出现在它的左子树中,那最低的共同父结点也出现在它的左子树中。如果两个结点都出现在它的右子树中,那最低的共同父结点也出现在它的右子树中。如果两个结点一个出现在左子树中,一个出现在右子树中,那当前的结点就是最低的共同父结点。
// 最近公共祖先
TreeNode *LCA(TreeNode *root,TreeNode *u,TreeNode *v){
    // 空树
    if(root == nullptr || u == nullptr || v == nullptr){
        return nullptr;
    }//if
    // check whether left child has u and v
    bool leftHasNode1 = false;
    bool leftHasNode2 = false;
    if(root->left){
        leftHasNode1 = Exits(root->left,u);
        leftHasNode2 = Exits(root->left,v);
    }//if
    if(leftHasNode1 && leftHasNode2){
        if(root->left == u || root->left == v){
            return root;
        }//if
        return LCA(root->left,u,v);
    }//if
    // check whether right child has u and v
    bool rightHasNode1 = false;
    bool rightHasNode2 = false;
    if(root->right){
        if(!leftHasNode1){
            rightHasNode1 = Exits(root->right,u);
        }//if
        if(!leftHasNode2){
            rightHasNode2 = Exits(root->right,v);
        }//if
    }//if
    if(rightHasNode1 && rightHasNode2){
        if(root->right == u || root->right == v){
            return root;
        }//if
        return LCA(root->right,u,v);
    }//if
    // both left and right child has
    if((leftHasNode1 && rightHasNode2) || (leftHasNode2 && rightHasNode1)){
        return root;
    }//if
    return nullptr;
}

接着我们来分析一下这个方法的效率。函数 Exits 的本质就是遍历一棵树,其时间复杂度是 O(n) n 是树中结点的数目)。由于我们根结点开始,要对每个结点调用函数 Exits 。因此总的时间复杂度是 O(n^2)
我们仔细分析上述代码,不难发现我们判断以一个结点为根的树是否含有某个结点时,需要遍历树的每个结点。
接下来我们判断左子结点或者右结点为根的树中是否含有要找结点,仍然需要遍历。第二次遍历的操作其实在前面的第一次遍历都做过了。
由于存在重复的遍历,本方法在时间效率上肯定不是最好的。

【版本二】
前面我们提过如果结点中有一个指向父结点的指针,我们可以把问题转化为求两个链表的共同结点。现在我们可以想办法得到这个链表。我们在本面试题系列的第4题中分析过如何得到一条中根结点开始的路径。我们在这里稍作变化即可:

/**--------------------------------
*   日期:2015-02-02
*   作者:SJF0115
*   题目: 从根节点到目标节点的路径
------------------------------------**/
bool NodePath (TreeNode* root,TreeNode* node,vector<TreeNode*> &path) {
    if(root == node) {
        return true;
    }//if
    path.push_back(root);
    bool isExits = false;
    // left sub tree
    if(root->left) {
        isExits = NodePath(root->left,node,path);
    }//if
    if(!isExits && root->right) {
        isExits = NodePath(root->right,node,path);
    }//if
    if(!isExits) {
        path.pop_back();
    }//if
    return isExits;
}

由于这个路径是从跟结点开始的。最低的共同父结点就是路径中的最后一个共同结点:

/*-------------------------------------
*   日期:2015-04-24
*   作者:SJF0115
*   题目: 最小公共祖先
*   博客:
------------------------------------*/
#include <iostream>
#include <vector>
using namespace std;

struct TreeNode{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode *parent;
    TreeNode(int x):val(x),left(nullptr),right(nullptr),parent(nullptr){}
};
/*
题目:
给定两个节点,求它们在一棵二叉树中的最小公共祖先。
每个节点除了有左右节点之外,还有一个指向父节点的指针
*/
// 找到u,v在二叉树中对应的节点
bool FindPath(TreeNode* root,TreeNode* node,vector<TreeNode*> &path){
    if(root == nullptr || node == nullptr){
        return false;
    }//if
    path.push_back(root);
    // 找到目标节点
    if(root == node){
        return true;
    }//if
    bool isFind = false;
    // 左子树中查找
    if(root->left){
        isFind = FindPath(root->left,node,path);
    }//if
    // 右子树查找(左子树中未找到的情况下)
    if(!isFind && root->right){
        isFind = FindPath(root->right,node,path);
    }//if
    // 左右子树均未找到
    if(!isFind){
        path.pop_back();
    }//if
    return isFind;
}

TreeNode* LCA(TreeNode* root,TreeNode* u,TreeNode* v){
    vector<TreeNode*> path1,path2;
    // 根节点到u的路径
    bool isFound1 = FindPath(root,u,path1);
    // 根节点到v的路径
    bool isFound2 = FindPath(root,v,path2);
    // 二叉树中是否存在uv节点
    if(!isFound1 || !isFound2){
        return nullptr;
    }//if
    int size1 = path1.size();
    int size2 = path2.size();
    TreeNode* node = nullptr;
    for(int i = 0,j = 0;i <= size1 && j <= size2;++i,++j){
        if(i == size1 || j == size2 || path1[i] != path2[j]){
            node = path1[i-1];
            break;
        }//if
    }//for
    return node;
}

int main(){
    TreeNode* root = new TreeNode(0);
    TreeNode* node1 = new TreeNode(1);
    TreeNode* node2 = new TreeNode(2);
    TreeNode* node3 = new TreeNode(3);
    TreeNode* node4 = new TreeNode(4);
    TreeNode* node5 = new TreeNode(5);
    TreeNode* node6 = new TreeNode(6);

    root->left = node1;
    root->right = node2;
    node1->left = node3;
    node1->right = node4;
    node2->right = node5;
    node4->left = node6;

    vector<TreeNode*> path;
    if(FindPath(root,node5,path)){
        for(int i = 0;i < path.size();++i){
            cout<<path[i]->val<<" ";
        }//for
        cout<<endl;
    }//if
    path.clear();
    if(FindPath(root,node6,path)){
        for(int i = 0;i < path.size();++i){
            cout<<path[i]->val<<" ";
        }//for
        cout<<endl;
    }//if
    TreeNode* parent = LCA(root,node5,node6);
    cout<<parent->val<<endl;
    return 0;
}


这种思路的时间复杂度是 O(n) ,时间效率要比第一种方法好很多。但同时我们也要注意到,这种思路需要两个链表来保存路径,空间效率比不上第一个方法。









目录
相关文章
|
4月前
|
算法 vr&ar Windows
Tarjan算法求LCA(最近公共祖先)
Tarjan算法求LCA(最近公共祖先)
19 0
|
6月前
|
算法 Python
Python算法——最近公共祖先
Python算法——最近公共祖先
46 0
|
6月前
|
算法
代码随想录算法训练营第二十一天 | LeetCode 235. 二叉搜索树的最近公共祖先、701. 二叉搜索树中的插入操作、450. 删除二叉搜索树中的节点
代码随想录算法训练营第二十一天 | LeetCode 235. 二叉搜索树的最近公共祖先、701. 二叉搜索树中的插入操作、450. 删除二叉搜索树中的节点
45 0
|
6月前
|
算法
代码随想录算法训练营第二十天 | LeetCode 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先
代码随想录算法训练营第二十天 | LeetCode 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先
31 0
|
11月前
|
算法 开发工具 git
LeetCode算法小抄-- 最近公共祖先 和 完全二叉树的节点个数
LeetCode算法小抄-- 最近公共祖先 和 完全二叉树的节点个数
【夯实算法基础】最近公共祖先
【夯实算法基础】最近公共祖先
【夯实算法基础】最近公共祖先
|
算法
leetcode算法235.二叉搜索树的最近公共祖先
当给定一个二叉搜索树时, 如何找到该树中两个指定节点的最近公共祖先?本文带大家解决这个问题。
87 0
leetcode算法235.二叉搜索树的最近公共祖先
|
4天前
|
算法 数据安全/隐私保护 计算机视觉
基于二维CS-SCHT变换和LABS方法的水印嵌入和提取算法matlab仿真
该内容包括一个算法的运行展示和详细步骤,使用了MATLAB2022a。算法涉及水印嵌入和提取,利用LAB色彩空间可能用于隐藏水印。水印通过二维CS-SCHT变换、低频系数处理和特定解码策略来提取。代码段展示了水印置乱、图像处理(如噪声、旋转、剪切等攻击)以及水印的逆置乱和提取过程。最后,计算并保存了比特率,用于评估水印的稳健性。
|
1天前
|
算法 计算机视觉
基于高斯混合模型的视频背景提取和人员跟踪算法matlab仿真
该内容是关于使用MATLAB2013B实现基于高斯混合模型(GMM)的视频背景提取和人员跟踪算法。算法通过GMM建立背景模型,新帧与模型比较,提取前景并进行人员跟踪。文章附有程序代码示例,展示从读取视频到结果显示的流程。最后,结果保存在Result.mat文件中。
|
1天前
|
资源调度 算法 块存储
m基于遗传优化的LDPC码OMS译码算法最优偏移参数计算和误码率matlab仿真
MATLAB2022a仿真实现了遗传优化的LDPC码OSD译码算法,通过自动搜索最佳偏移参数ΔΔ以提升纠错性能。该算法结合了低密度奇偶校验码和有序统计译码理论,利用遗传算法进行全局优化,避免手动调整,提高译码效率。核心程序包括编码、调制、AWGN信道模拟及软输入软输出译码等步骤,通过仿真曲线展示了不同SNR下的误码率性能。
7 1