算法——树的最近公共祖先(微软面试)

题目一:二叉树的最近公共祖先,无父指针。

题目链接:. - 力扣(LeetCode)

方式一:使用递归解决。

既在左子树找p和q,又在右子树找p和q。 只要找到p或q的其中一个就可以返回,如果left和right都不为空,说明左右子树各找到p和q当中的一个,那么p和q在root的两侧,此时root必为left和right的最近公共祖先,向上返回即可。 如果left不为空,说明p,q在左子树。 如果right不为空,说明p,q在右子树。 left和right都为空,说明找不到,返回空。

/**
 * 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:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
       if (root == nullptr || root == p || root == q) {
            //只要当前根节点是p和q中的任意一个,就返回(因为不能比这个更深了,再深p和q中的一个就没了)
            return root;
        }
        //根节点不是p和q中的任意一个,那么就继续分别往左子树和右子树找p和q
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        //p和q都没找到,那就没有
        if(left == nullptr && right == nullptr) {
            return nullptr;
        }
        //左子树没有p也没有q,就返回右子树的结果
        if (left == nullptr) {
            return right;
        }
        //右子树没有p也没有q就返回左子树的结果
        if (right == nullptr) {
            return left;
        }
        //左右子树都找到p和q了,那就说明p和q分别在左右两个子树上,所以此时的最近公共祖先就是root
        return root;  
    }
};

方式二:使用哈希表存储每个节点的父节点(若题目给的二叉树定义中包含父节点则非常好用)

我们可以用哈希表存储所有节点的父节点,然后我们就可以利用节点的父节点信息从 p 结点开始不断往上跳,并记录已经访问过的节点,再从 q 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是我们要找的最近公共祖先。

算法:

从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
同样,我们再从 q 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。

class Solution {
public:
    unordered_map<int, TreeNode*> fa;
    unordered_map<int, bool> vis;
    void dfs(TreeNode* root){
        if (root->left != nullptr) {
            fa[root->left->val] = root;
            dfs(root->left);
        }
        if (root->right != nullptr) {
            fa[root->right->val] = root;
            dfs(root->right);
        }
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        fa[root->val] = nullptr;
        dfs(root);
        while (p != nullptr) {
            vis[p->val] = true;
            p = fa[p->val];
        }
        while (q != nullptr) {
            if (vis[q->val]) return q;
            q = fa[q->val];
        }
        return nullptr;
    }
};

题目二:二叉搜索树

很简单,二叉搜索树,即左子树的节点值都比根节点小,右子树的节点值都比根节点大,判断待求两个节点的值,如果都比根节点小,则最近公共祖先必在根节点的左子树上,来到左子树上去判断。如果都比跟节点大,则来到右子树上判断。如果恰好处在两个节点值之间,则说明一个在左子树,一个在右子树,该节点则为最近公共祖先。

题目三:多叉树的最近公共祖先。

题目链接:【模板】最近公共祖先(LCA) - 洛谷

见题解

b站:【D09 倍增算法 P3379【模板】最近公共祖先(LCA)】 https://www.bilibili.com/video/BV1vg41197Xh/?share_source=copy_web&vd_source=c83e007dbefdc4a5ea41a926c1309a49

#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<stdio.h>
#include<vector>
#define maxn 500500
using namespace std;
///隶属邻接表 
struct Edge{                    //邻接表的结构体 
    int from,to;
}edges[2*maxn];                 //边要乘2,因为是无向图 ; 
int first[maxn],next[2*maxn];   //同理; 
int read(){                        //读入优化,可以照着这个模板来写,这个还算写的比较好看。 
    int re=0;
    char ch=getchar();
    while (ch<'0' || ch>'9') ch=getchar();
    while (ch>='0' && ch<='9'){ 
        re=re*10+ch-'0'; 
        ch=getchar();
    }
    return re;
}
///
///全局变量 
int n,m;
int root;
int height[maxn];
float log2n;
///
///隶属LCA的全局变量 
int f[maxn][20];// 
int have[maxn];                           //have,有没有找过,这都是套路 。 
void dfs(int u,int h){                 //u代表点的标号,h代表高度。 
    int v;
    height[u]=h;
    for(int i=1;i<=log2n;i++) {
        if(h<=(1<<i)) break;              //由于i是从小到大计算的,故(1<<i)>=h 时可直接退出。请务必想清楚是<=  还是=。
        f[u][i] = f[ f[u][i-1] ][i-1]; //动规计算。同样也是一切倍增算法的核心。 
    }
    int k=first[u];
    while(k!=-1){
        v=edges[k].to;
        if(!have[v]) {
            have[v]=1;        
            f[v][0]=u;                 //将要找的下一个点的父节点标为当前处理的节点u。 
            dfs(v,h+1);
        }
        k=next[k];
    }
}
int require_LCA(int a,int b){
    int da=height[a],db=height[b];
//第一步,将a,b两点移到同样的高度,只动高度大的那个点而不动高度小的那个点。 
    if(da!=db) {
        if(da<db){                   //保证a的高度是大于b的高度的。 
            swap(a,b);
            swap(da,db);
        }
        int d=da-db;
        for(int i=0;i<=log2n;i++) 
            if( (1<<i) & d) a=f[a][i]; //这里的位运算可以减少代码量
                                       //考虑到d是一个定值,而(1<<i)在二进制中只有第(i+1)位是1; 
                                       //那么d与(1<<i)如果某一位为1,那么表示可以向上移动, 
                                       //如果此时不移动,那么i增大了后就无法使height[a]==height[b]了 
    }
//第二步,找到某个位置i,在这个位置时,f[a][i]!=f[b][i],但再向上移动一步,a,b相同了 
//从log2n开始从大到小枚举i,如果超过了a,b的高度,则令i继续减小
//如果没有超过a,b的高度,那么就判断移动了后会不会让a==b,
//是,则i继续减小,否则,令此时的a=f[a][i],b=f[b][i]; 
    if(a==b) return b;
    int i=0;
    for(i=log2n;i>=0;i--) {
        if(height[ f[a][i] ]<0) continue;
        if( f[a][i]==f[b][i] ) continue;
        else a=f[a][i],b=f[b][i];        //顺便一提,在第二步任何地方没有break;
                                       //我就是因为在这里写了一个break,然后找了我两个小时啊。 
    }    
    return f[a][0];
}
/
///据说从主函数开始阅读是个好习惯。 
int main(){
//    freopen("in2.txt","r",stdin);
    n=read();m=read();root=read();
    memset(first,-1,sizeof(first));
    memset(next,-1,sizeof(next));
    int s,t;
    int dsd=2*(n-1);
    for(int i=1;i<=dsd;i+=2) {
        s=read();t=read();      //读入优化。 
        edges[i].from=s;
        edges[i].to=t;
        edges[i+1].from=t;
        edges[i+1].to=s;
        next[i]=first[s];
        first[s]=i;
        next[i+1]=first[t];
        first[t]=i+1;
    }
    // 以上是邻接表,在此不再赘述。 
    log2n=log(n)/log(2)+1;        //C++计算log是自然对数,我们要用的以2为底的对数,故要除以log(2); 
                                  //对无理数加上1或是0.5是个好习惯,可以减小误差; 
    memset(have,0,sizeof(have));
    memset(height,0,sizeof(height));
    memset(f,-1,sizeof(f));
    have[root]=1;                //fa[][]和height[]要在dfs理进行计算,不然根本找不到某个非根节点的父亲是谁; 
    dfs(root,1);                
    for(int i=1;i<=n;i++){
        for(int j=0;j<=log2n;j++) {
            if(height[i] <=(1<<j) ) break;
        }
    }
    for(int i=0;i<m;i++) {      //应对要求进行求解。 
        s=read();t=read();
        int y=require_LCA(s,t);
        printf("%d\n",y);
    }
    return 0;
}
  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值