【题目概述】:
给定一棵二叉树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 };
分析时间复杂度:
层次遍历为节点编号时,共访问n次节点。而在寻找p,q的最近公共祖先时,比较坏的情况下,p,q均为叶子节点,而最近公共祖先为根节点,这样基于p的节点访问次数和基于q的节点访问次数也分别近似为n。总的时间复杂度为O(n)。
在LeetCode的测试平台上,方法一和二的耗时均为24ms。
【总结】
方法一:先根遍历,自顶向下,递归实现;方法二:层次遍历,自底向上,非递归实现。我认为方法一好一些,因为它具有更深邃的设计思想和更简洁,更简短,更清晰的代码。方法二以层次遍历的方式为树形结构进行线性排序,使反向寻找双亲成为可能。扩展一些,沿用方法二“为节点标号”的思想,如果我们以中根遍历的方式为节点标号,倘若以编号为关键字,则一棵普通树可以转化为一棵搜索二叉树,则可用此题的兄弟题LeetCode235."Lowest Common Ancestor of a Binary Search Tree"的方法解决。虽然我用中根遍历的方式失败了,但我仍认为这是一种可行的方法。
【附】如果你看到了我的这第一篇博客,如果我的阐释能对你有些启发,那么这些文字便有了意义。愿我们共同探索,共同进步!