[LeetCode Solution 1] 236.Lowest Common Ancestor of a Binary Tree

 

【题目概述】:

  给定一棵二叉树T,以及二叉树中的两个节点p和q,试求p和q的最近公共祖先。而所谓p和q最近公共祖先,是指同时拥有p和q两个子孙的所有节点中,深度最大的节点(这里认为一个节点可以是自己的子孙)。

  具体说来要完成如下的一个函数:

  TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q);

  其中root为指向所给树根节点的指针;p,q为指向待考察的两节点的指针;要求返回指向最近公共祖先的指针;

  题目同时给出了如下的数据结构表示树中结点:
      Definition for a binary tree node.
      struct TreeNode {
      int val;
      TreeNode *left;
      TreeNode *right;
      TreeNode(int x) : val(x), left(NULL), right(NULL) {}

  };

 

【可行解题方法】:

方法一:基于先根遍历的深度搜索算法

  这不是我想出来的方法,而是一种网上流传的方法,先呈上C++代码,稍后再分析:

  

 1 class Solution {
 2  public:
 3     TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
 4          if (root == NULL) return NULL;
       //此时已无节点可供向下搜索,也同时表明此条路径上无p或q,返回空指针
5 if (root == p || root == q) return root;
       //如果root已为p或q,则不用再搜索以root为根节点的子树了,直接返回p或q的指针
6 TreeNode *L = lowestCommonAncestor(root->left, p, q); 7 TreeNode *R = lowestCommonAncestor(root->right, p, q);
8 if (L && R) return root; 9 return L ? L : R;
10 } 11 };

    对于每一层递归,L和R接受了更深一层的递归返回的值,将L和R结合起来看,不外乎有四类可能结果:

    1. L = NULL,R = NULL; 表明以root->left为根节点的子树和以root->right为根节点的子树均不包含节点p和q。那么,当前层递归向上一层递归返回的值也是NULL。表明以root为根节点的子树不含p和q

    2. L = NULL, R指向p; 表明以root->left为根节点的子树不包含节点p和q,但以root->right为根节点的子树仅包含节点p。那么,当前层递归向上一层递归返回的指针指向p。表明以root为根节点的子树仅含节点p

    3. L指向p,R指向q;表明以root->left为根节点的子树仅包含节点p,而以root->right为根节点的子树仅包含节点q。此时可以断定,当前的root就是p和q的最近公共祖先。我们不妨记当前的root为ans(意为题目的答案)。那么,当前层递归向上一层递归返回的指针指向ans。表明以root为根节点的子树含节点p和q

    4.L = NULL,R指向ans,表明以root->left为根节点的子树不包含节点p和q,但以root->right为根节点的子树同时包含p和q。那么,当前层递归向上一层递归返回的指针指向ans。表明以root为根节点的子树含节点p和q(注意此时的root不同于ans,是p和q的公共祖先,但已不是最近公共祖先了)。但没有关系, 指向ans的指针由于返回至上一层递归而得到了保留。这样,我们总可以在最上层函数的出口处得到指向ans的指针。

       显然,由于p和q,L和R的等价性,可能的结果还有更多。比如:L = NULL, R指向q;L指向ans, R = NULL等。但不会超出如上所说的四种结果类型。为了更好的显示深搜回溯时的深层函数向浅层函数的传值情况,附示例如下:

  

  此树中节点既可以认为是树中的实际节点,又可认为是以该节点为root参数的那一层函数调用,而分支上的标示则显示了深层递归向浅层递归所传的值。(深层函数值先产生,浅层函数值后产生。)  

  分析时间复杂度:
      我们设用此法解节点数为n的树,所进行的总操作步数为F(n);
      函数内部有两个if语句和两个return语句,以及两个递归调用,假设这两个递归调用所解树的规模均为n/2
      则有递推式  F(n) = 2F(n/2) + 4;
      亦即  令W(n) = F(n) + 4
      上式变为 W(n) = 2*W(n/2),  显然W(n) = O(n),进而F(n) = O(n);
      因此此法的时间复杂度为O(n);

      这样的是时间复杂度分析是比较粗糙的,但我想可以说明问题了。

 

方法二:基于层次遍历的搜索算法

      这是我的自己的想法。
      从最近公共祖先的定义出发,一个自然的想法恐怕不是如方法一中的自顶向下的寻找策略,而是将p和q分别自底向上地寻找祖先,直到碰到一个他们公共的祖先,就是最近公共祖先了。

  想法不坏,但问题的关键在于如何找到一个节点p的双亲。由于树中节点的指针指向的是孩子而非双亲,一个比较高效的寻找双亲的方案并不是那么显而易见。首先,可以想到枚举其他节点,判断他们的孩子是否是p;可是对于树状结构,怎么按照一定顺序获取树中结点?

  某一种遍历方式可以做到按一定顺序获取节点,这里之所以用层次遍历,是因为非递归通常比递归快一点点,而层次遍历是最易写作非递归形式的遍历方式。

  层次遍历可以按照由浅到深,由上到下的顺序为节点编号,倘若我们开出一个非循环队列queue(就是数组),queue[i]存储在层次遍历方式下编号为i的节点的地址。则借由queue数组,我们可以通过编号指代节点了。假使我们知道了节点p的编号为j,则我们遍历编号为1..j(不包括j)的节点,就会找到p的双亲了。
    但遍历编号为1..j - 1的节点会有很多无效搜索,实际上p的双亲只可能位于比p浅一层的节点之中。为了描述“比p浅一层的节点”这样一个搜索范围,
我们再引入数组level,level[i]表示树的第i层中,从左往右数,第一个节点的编号。假使我们知道了节点p处在第k层中,我们遍历编号为level[k - 1]..level[k](不包括level[k])的节点,就会找到p的双亲了(可参考下图示例)。而找祖先这件事也可通过多次找双亲完成。

  
    此外,在找祖先的时候可以采用如下策略减少寻找双亲的次数。假使开始给定的p处在c1层,q处在c2层,c1 > c2,即p节点位于树的更深处,则先找到p在c2层的祖先p’。此后,同时找p’和q在更上一层的双亲,并将双亲进行比较,双亲相同即为最近公共祖先,若不同,则找再上一层双亲并比较。如此循环往复,总可以找到最近公共祖先。

  方法二的思路不困难,我想读者根据以上内容是可以自行实现此法的。不过还是把源码附上了,可参考其中的细节:

  

  1 class Solution {
  2 public:
  3     void markAllNode(TreeNode* root, TreeNode*p, TreeNode* q, TreeNode** queue, 
  4                      int* level, int& origin_p, int& origin_q, int& level_p, int& level_q)
  5     //进行层次遍历,为所有节点编号
  6     {
  7         int front = 0,//指向队首元素下标 
  8             rear = 0,//指向队尾元素下标
  9             current_level = -1;//指示当前待考察节点所在的层数,为了后面的循环处理方便,设为-1
 10         queue[0] = root;//根节点事先入队
 11         level[0] = 0;//处在第0层的第一个节点是编号为0的根节点(也是处在0层的唯一节点)
 12         if (p == root)
 13         {
 14             origin_p = 0;
 15             level_p = 0;
 16         }
 17         if (q == root)
 18         {
 19             origin_q = 0;
 20             level_q = 0;
 21         }
 22         while (front <= rear)
 23         {
 24             if (level[current_level + 1] == front)
 25                 //发现待扩展节点为新一层的第一个节点
 26             {
 27                 current_level++;//更新当前考察节点所在层数
 28                 while (!queue[front]->left && !queue[front]->right)
 29                 {
 30                     front++;
 31                     if (front > rear)
 32                     {
 33                         return;
 34                     }
 35                 }
 36                 level[current_level + 1] = rear + 1;//预设下一层第一个节点编号
 37             }
 38             if (!queue[front]->left && !queue[front]->right)
 39             {
 40                 front++;
 41                 continue;
 42             }//欲扩展的节点无孩子
 43             if (queue[front]->left)//左孩子存在
 44             {
 45                 rear++;
 46                 queue[rear] = queue[front]->left;
 47                 if (p == queue[rear])
 48                 {
 49                     origin_p = rear;
 50                     level_p = current_level + 1;
 51                 }//判断p是否指向该左孩子
 52                 if (q == queue[rear])
 53                 {
 54                     origin_q = rear;
 55                     level_q = current_level + 1;
 56                 }//判断q是否指向该左孩子
 57             }
 58             if (queue[front]->right)//右孩子存在
 59             {
 60                 rear++;
 61                 queue[rear] = queue[front]->right;
 62                 if (p == queue[rear])
 63                 {
 64                     origin_p = rear;
 65                     level_p = current_level + 1;
 66                 }
 67                 if (q == queue[rear])
 68                 {
 69                     origin_q = rear;
 70                     level_q = current_level + 1;
 71                 }
 72             }
 73             front++;
 74             //front指向下一个待扩展的节点
 75         }
 76     }
 77     int findLatestAncestor(TreeNode** queue, int* level, int descendant, int level_d)
 78     //寻找descendant所表示的节点的双亲,level_d表明节点所处层数
 79     //返回双亲在层次遍历中的编号
 80     {
 81         for (int i = level[level_d - 1]; i < level[level_d]; i++)
 82         //遍历上一层节点
 83         {
 84             if (queue[i]->left == queue[descendant] || queue[i]->right == queue[descendant])
 85             {
 86                 return i;
 87             }
 88         }
 89     }
 90     TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 
 91     {
 92         TreeNode* queue[10000];
 93         //queue[i]存放对树进行层次遍历中,编号为i的节点的地址
 94         int level[1000];
 95         //level[i]存放树的第i层节点中,编号最小的结点,假设根结点所处层数为0
 96         int origin_p, origin_q, level_p, level_q;
 97         //origin_p为p指针所指向的节点在层次遍历中的编号,
 98         //origin_q为q指针所指向的节点在层次遍历中的编号,
 99         //level_p为p指针所指向的节点在树中所处的层次,
100         //level_q为q指针所指向的节点在树中所处的层次,
101         markAllNode(root, p, q, queue, level, origin_p, origin_q, level_p, level_q);
102         while (level_p > level_q)
103         {
104             origin_p = findLatestAncestor(queue, level, origin_p, level_p);
105             level_p--;
106         }//当origin_p所指向的节点在origin_q所指向的节点下方时,寻找origin_p的与origin_q处于同一层次的祖先
107         while (level_p < level_q)
108         {
109             origin_q = findLatestAncestor(queue, level, origin_q, level_q);
110             level_q--;
111         }//当origin_q所指向的节点在origin_p所指向的节点下方时,寻找origin_q的与origin_p处于同一层次的祖先
112         while (origin_p != origin_q)
113         {
114             origin_p = findLatestAncestor(queue, level, origin_p, level_p);
115             level_p--;
116             origin_q = findLatestAncestor(queue, level, origin_q, level_q);
117             level_q--;
118         }//每次循环均同时寻找origin_p和origin_q各自的双亲,并及时进行比较,判断是否找到了共同祖先
119 
120         return queue[origin_p];
121         //返回指向p,q共同祖先的指针
122     }
123 };
View Code

  分析时间复杂度:

  层次遍历为节点编号时,共访问n次节点。而在寻找p,q的最近公共祖先时,比较坏的情况下,p,q均为叶子节点,而最近公共祖先为根节点,这样基于p的节点访问次数和基于q的节点访问次数也分别近似为n。总的时间复杂度为O(n)。

  在LeetCode的测试平台上,方法一和二的耗时均为24ms。

  【总结】

  方法一:先根遍历,自顶向下,递归实现;方法二:层次遍历,自底向上,非递归实现。我认为方法一好一些,因为它具有更深邃的设计思想和更简洁,更简短,更清晰的代码。方法二以层次遍历的方式为树形结构进行线性排序,使反向寻找双亲成为可能。扩展一些,沿用方法二“为节点标号”的思想,如果我们以中根遍历的方式为节点标号,倘若以编号为关键字,则一棵普通树可以转化为一棵搜索二叉树,则可用此题的兄弟题LeetCode235."Lowest Common Ancestor of a Binary Search Tree"的方法解决。虽然我用中根遍历的方式失败了,但我仍认为这是一种可行的方法。

 

  【附】如果你看到了我的这第一篇博客,如果我的阐释能对你有些启发,那么这些文字便有了意义。愿我们共同探索,共同进步!   

posted @ 2015-11-19 22:33  mzj14  阅读(182)  评论(0编辑  收藏  举报