title: 236. 二叉树的最近公共祖先
tags:
notebook: leetcode
#### 题目介绍
###### 题目难度: medium
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
#### 题目样例
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
算法思想
LCA查找p、q节点的公共祖先
对于二叉树的双亲表示法来说,二叉树相当于一个出度最大为二,入度最大为1的有向图
因此对于节点 Node来说仅与父亲有一条路径,而不断的网上回溯,我们发现仅存在一条
从根节点到Node的路径,而所有前置节点即为Node的祖先,并且根节点到Node的路径
是一个线性表(元素间具有前后关系)
那么对于节点p,q来说显然他们的公共祖先即为,
R
o
u
t
e
(
r
o
o
t
,
p
)
∩
R
o
u
t
e
(
r
o
o
t
,
q
)
Route(root,p)\cap Route(root,q)
Route(root,p)∩Route(root,q)
显然为这两个集合的交集 而之前我们说过路径是一个线性表,
因此我们就有了一个大概的算法思路了
1.获得根节点到p,q的路径
2.找到路径的最后一个相同元素
算法实现1
我们先进行递归的写法
对于节点Node 作为根的子树来说,
有三种可能
1.当前子树存在公共祖先
2.当前子树存在一个目标节点
3.当前子树不存在目标节点
因此对于Node来说
1.递归左右子树获取左右子树的对于p,q的公共节点
2.如果左右子树都不为空返回当前节点(不会存在多个相同节点,因此如果左右子树非空当前节点必为祖先,否则应该在同一子树内)
3.如果左右子树存在一个目标节点且当前节点为其中一个目标节点则当前节点为祖先
4.反之则当前子树只存在一个节点则返回这个节点
5.如果当前子树不存在目标节点返回空
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root)
return root;
TreeNode*left = lowestCommonAncestor(root->left,p,q);
if(left==p||left==q){//这里做了一下优化如果左子树+当前集合的节点中已经存在祖先则直接返回
if(root == p||root == q)
return root;
}
else if(left)
return left;
TreeNode*right = lowestCommonAncestor(root->right,p,q);
if(left||right){
if(left&&right)
return root;
return (root==p||root == q)?root:(left?left:right);
}
return (root==p||root==q)?root:NULL;
}
算法实现2 非递归
那么接下来就是非递归的实现了
如之前所说这里主要是两步的实现 **1、获取p,q路径 2.顺序表求交集 **
这里由于dfs的回溯性采用栈记录路径
递归最大的特点是保存未处理状态,而这里本身我们已经使用了两个栈来记录路径了实际执行到第i个节点时,Rp,Rq已经尽可能的保存了相应的路径(必然存在p,q的祖先),也就是这两个栈其实已经完全是dfs遍历栈了。因此处理完的最优状态保存好了只剩一个未处理状态需要保存(很幸运由于同时dfs,p,q可以共享未处理状态)
至于为什么需要未处理状态还是因为dfs依旧是线性的,是根据选取的点开始遍历,因此仅需将未访问点进行存档,同时设置标记提示当前层遍历结束(或者说该点的所有边遍历结束,(又回到图了)),因此得到最优结果或者结果不存在所有要回溯把结果拿回去或者退档。
所以我们需要增加一个NULL指针作为回溯标记(如果比较熟的话没错这就是后序遍历)
这里的压栈顺序是:(这里因为路径的关系显然我不能访问到空节点,后面会讲这里的特殊处理)
1.如果Rp,Rq栈顶非pq说明路径未找完,则将当前节点压栈
2.判断右孩子是否为空非空先压一个NULL再入栈右孩子(为空直接压右孩子,那显然图的话直接遍历边表,当然如果出度大于零先压空)
3.这里有点小优化是,判断右孩子是否为p,q如果是直接入栈,(幸运的话当前可能Rp,Rq满栈即路径找到,直接退出不递归左子树了节省时间),概率节省时间。
4.判断左孩子是否为空,非空则更新(因为我们先遍历左子树,如果喜欢右边所有顺序调个)
5.判断左孩子为空这里就需要特殊说明了,因为对于不够平衡的二叉树,特别是平衡因子>1的子树来说右节点为空的个数太多,所以存在缓存栈有连续的空指针,这里的处理就是之前说过的保证指针非空。路径不会压入空指针。
步骤一左子树为空,节点必然更新为右孩子,且缓存栈出栈(之前压过)
步骤二若节点更新为空,则回溯标记,开始向上回溯,如果Rp,Rq栈顶非p,q说明没找到路径则回溯退栈,并出栈缓存栈更新节点
步骤三若节点为空且缓存栈非空则继续退栈重复2
这里还是找个例子直观的看一下 。
后序遍历
root = 5 stack = [5] cache = [6,NULL] result = []
root = 3 stack = [3,5] cache = [4,NULL,6,NULL] result = []
root = 2 stack = [2,3,5] cache = [NULL,4,NULL,6,NULL] result = []
root = 1 stack = [1,2,3,5] cache = [NULL,NULL,4,NULL,6,NULL] result = []
root = NULL stack = [2,3,5] cache = [NULL,4,NULL,6,NULL] result = [1]
root = NULL stack = [3,5] cache = [4,NULL,6,NULL] result = [1,2]
root = 4 stack = [4,3,5] cache = [NULL,NULL,6,NULL] result = [1,2]
root = NULL stack = [3,5] cache = [NULL,6,NULL] result = [1,2,4]
root = NULL stack = [5] cache = [6,NULL] result = [1,2,4,3]
root = NULL stack = [5] cache = [NULL] result = [1,2,4,3,6]
root = NULL stack = [] cache = [] result = [1,2,4,3,6,5]
非递归后序
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(!root)
return vector<int>(0);
stack<TreeNode *> p,cache;
vector<int> num;
p.push(root);
if(root->right){
cache.push(NULL);
}
cache.push(root->right);
if(root->left)
root = root->left;
else{
root = root->right;
cache.pop();
}
while(root||!cache.empty()){
if(root->right){
cache.push(NULL);
}
cache.push(root->right);
p.push(root);
if(root->left)
root = root->left;
else{
root = root->right;
cache.pop();
while(!root&&!cache.empty()){
num.push_back(p.top()->val);
p.pop();
root = cache.top();
cache.pop();
}
}
}
num.push_back(p.top()->val);
return num;
}
};
p = 5 q = 4
root = 3 Rp = [3] Rq = [3] cache =[1,NULL]
root = 5 Rp = [5,3] Rq = [5,3] cache =[2,NULL,1,NULL]
root = 6 Rp = [5,3] Rq = [6,5,3] cache =[2,NULL,1,NULL]
root = NULL Rp = [5,3] Rq = [5,3] cache =[2,NULL,1,NULL]
root = 2 Rp = [5,3] Rq = [4,2,5,3] cache =[4,NULL,2,NULL,1,NULL]
这里我们要讨论的是另一个问题了,对于树内高度一致的两个节点,向上移动h层则高度保持一致,显然如果二者高度为H,则移动H次必然会走到根节点,而由于公共前缀的关系,需要从相同层出发移动<=H层则必然会走到他们的公共路径(即节点相等)
复1复1存栈对于两个路径第一步,是让路径长度一致
让长的那个先向上爬到与短路径相同的高度上,再一起爬
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode *> Rp,Rq,cache;
cache.push(root->right);
//预处理避免重复运算以及栈空取栈顶报错
Rp.push(root);
Rq.push(root);
if(root->right == p)
Rp.push(p);
if(root->right == q)
Rq.push(q);
if(root->right)
cache.push(NULL);
cache.push(root);
//设定NULL为回溯标志因次如果非空则先压空
if(root->left){
root = root->left;
}else{
root = cache.top();
cache.pop();
}
while(root||!cache.empty()){
if(Rp.top()!=p)
Rp.push(root);
if(Rq.top()!=q)
Rq.push(root);
else if(Rp.top()==p)//两个栈满结束
break;
//预处理避免重复运算
if(root->left){
if(root->right)
cache.push(NULL);
cache.push(root->right);
root = root->left;
}else{
if(root->right)
cache.push(NULL);
root = root->right;
while(!root&&!cache.empty()){//空指针则回溯
if(Rq.top()!=q)
Rq.pop();
if(Rp.top()!=p)
Rp.pop();
root = cache.top();
cache.pop();
}
}
}
while(Rp.size()>Rq.size()){
Rp.pop();
}
while(Rq.size()>Rp.size()){
Rq.pop();
}
while(Rp.top()!=Rq.top()){
Rp.pop();
Rq.pop();
}
return Rp.top();
}