LCA-最小公共父节点

有一个普通二叉树,AB分别为两个子节点,求AB最近(深度最大)的公共父节点。
此题仍然是一个老题,有着多种解决方法,本文针对其中三种方法来进行分析总结。

这三种方法分别是:递归法,tarjan离线算法,RMQ在线算法。


递归法

递归法比较直观简单,思路如下:

  • 首先判定当前节点root是否是A节点或者B节点,若是的话直接返回该节点
  • 若不是,分别对root节点的左右子树进行递归查找最小公共父节点,若左右子树都返回了节点,那么表示当前节点就是最小公共父节点,若只有其中一个子树返回了结果,那么就返回该结果节点。

    参考代码如下:

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(!root)
            return NULL;

        if(root == p || root == q)
            return root;

        TreeNode* left = lowestCommonAncestor(root->left,p,q);
        TreeNode* right = lowestCommonAncestor(root->right,p,q);

        if(left == NULL) return right;
        if(right==NULL) return left;

        return root;
    }

在线算法
如上图中我们要查找节点4,3的最小公共父节点,那么上述代码的执行过程如下

  • 判断1是否4,3节点不是,查询1的左子树和右子树
  • 1的左子树中先判断2是否4,3节点不是,查找2的左右子树
    • 2的左子树 显然返回结果4,2的右子树返回结果NULL,显然2的左右子树并没有都返回树中的节点,因此2不是4,3的最小公共父节点,因此以2位根节点的1的左子树调用方法返回的是4.
  • 1的右子树判断3节点是否是4,3节点,发现是的,直接返回3节点。
  • 1的左右子树的递归调用都返回了结果,因此1就是节点4,3的最小公共父节点。

Tarjan离线算法

对于该算法,常常会有一个给定的树T和一个给定的查询集合P={(u,v)},我们需要确定P中每对的最小公共祖先。
算法为何说是离线的?因为针对所有的查询,我们都是在算法的一次执行过程中找到。

算法思路:

  • 根节点root开始搜索,每次递归搜索所有的子树
  • 当搜索到节点u时,创建一个由u本身组成的集合,这个集合的祖先为u自己。然后递归搜索u的所有儿子节点。每个儿子节点递归完毕之后,将儿子节点所在的集合和u所在的集合合并,再把该集合的祖先设定为节点u。
  • 每个节点的所有孩子都递归完毕后,将该节点设定为已经访问过,开始遍历所有u的查询,若发现查询的另外一个节点v也是标示为访问过的话,那么节点u和v的最小公共父节点即为节点v所在集合的祖先节点。

上述所有操作都是使用并查集高效完成的。因此时间复杂度,O(n)深度优先搜索所有节点的时间,搜索每个节点时会遍历这个节点相关的所有查询。如果总的查询个数为q,则总的复杂度为O(n+q)。

还是上图的树,我们从节点1开始,创建集合{1},然后递归到1的第一个孩子节点2,创建集合{2},之后继续深度递归到节点4,创建集合{4},完成节点4的访问后,将4设定为已访问,集合{4}与集合{2}合并,得到{4,2},将他们的祖先设为2,然后访问节点5,创建集合{5},完成之后将集合{2,4}和{5}合并,得到{2,4,5}然后将该集合的祖先设为2,若此时(5,4)是一个查询,那么就可以得到他们的最小公共祖先为2。之后合并{1}和{2,4,5}得到{1,2,4,5},祖先为1,访问3,继续访问6,完成之后,若(6,2)是一个查询,那么其最小公共祖先为1。剩下的操作类似。

伪代码如下(参考算法导论):

LCA(u)
  MAKE-SET(u)
  Find-Set(u).ancestor = u
  for each child v of u in T
      LCA(u)
      Union(u,v)
      Find-Set(u).ancestor = v
  u.color= Black
  for each node v such that (u,v) belongs to P
            if v.color ==Black
               print "The LCA of "u "and" v"is" Find-Set(v).ancestor

RMQ在线算法

无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。所以离线算法针对一次性较多查询的话比较实惠,但是每次只来一个查询,这时候多次调用离线算法就不值当了。在线算法处理这种情况比较合适。

之前文章里写过RMQ在线算法的原理了,这里就不加多说,本文主要介绍如何把RMQ算法和最小公共父节点问题结合起来。

算法思路:

LCA集合RMQ主要是通过DFS(深度优先搜索)完成。每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来,还是上面的树,DFS之后顺序为:1-2-4-2-5-2-1-3-6-3-1。那么要找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点,并且我们假定我们的树中每个父亲节点的标号都比孩子节点小,那么目标就转换为求该区间中标号最小的那个节点。

部分代码如下:

int ind[N];//用来存储每个节点在生成路径中最后一次出现的位置
vector<int> road;//存储DFS路径
void dfs(int s)
{
    road.push_back(s);
    for(int i = 0; i < V[s].size();i++){
        dfs(V[s][i]);
        road.push_back(s);
    }
}

得到路径之后,那么每个节点在生成路径中最后一次出现的位置可以用如下方式得到:

for(int i = 0; i < road.size();i++)
{
        ind[road[i]] = i + 1;
}

进行上述操作之后,对于每一对查询(u,v),我们再利用RMQ区间查询的算法输入ind[u],ind[v]就能得到其最小公共父节点了。


总结

本文总结了三种不同求解最小公共父节点的方法,本人曾在微软面试中被问及此题,答得并不是很好,若是能在面试中答出多种方法,那么一定会有大大的加分。

发布了42 篇原创文章 · 获赞 32 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章