二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
第一种情况:树结点中保存了双亲的位置和值域
如果二叉树结点中保存了双亲的位置和值域的话(比如双亲表示法或者是孩子双亲表示法),可以转换为两个链表求交点的问题
第二种情况:如果树是二叉搜索树(二叉排序树)
二叉搜索树特点:
- 根节点比左子树中所有节点都大,比所有右子树中的节点都小;
- 根据中序遍历可以得到一个有序序列
如果树是二叉搜索树(二叉排序树),此时分为如下几种情况(假设我们要找的是p,q的公共祖先):
- 如果p,q有一个在根的位置 || p,q分别在根节点的左右子树中(q<root && p>root)
最近的公共祖先一定是根节点 - p<root && q<root 即p,q都在root左子树
递归到左子树中查找 - p>root && q>root 即p,q都在root右子树
递归到右子树中查找
第三种情况:二叉树既不是孩子双亲表示法也不是二叉搜索树,就是一颗孩子表示法的普通二叉树
这种一般情况的求解方式参考情况1和情况2
方法一:(由情况一的思路扩展)
由于是孩子表示法,所以只能从根开始找到p所经结点,到q所经结点,but 我们查看最近公共祖先的时候是从结点往根开始找的,所以此处需要用到栈的特性,将途中所经结点保存再栈中
获取到结点所经的路径:
比如说获取根结点到E点所经路径
// 先获取到p,q结点的路径
public boolean nodePath(TreeNode root, TreeNode node, Stack<TreeNode> path){
if(null == root){
// 树为空返回null
return false;
}
// 将当前结点入栈
path.push(root);
if(node == root){
// 如果当前遍历到的结点root就是最终结点node
// root因为不断递归所以root不是最开始的根节点了,而是遍历到的当前结点
return true;
}
if(nodePath(root.left,node,path) || nodePath(root.right,node,path)){
// 在root的左子树中找node没找到就在右子树中找,找到了返回true
return true;
}
// 没在树中找到node,所以之前压栈的结点要删掉
path.pop();
return false;
}
得到p,q所经节点后,将他们存储在两个栈中,比较两个栈当中的元素(比较的时候类似于求链表相交,先让元素多的栈弹出元素,一直弹出到两个栈元素个数相等,然后开始比较两个栈的栈顶)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(null == root ||null == p ||null == q){
return null;
}
Stack<TreeNode> pPath = new Stack<>();
nodePath(root,p,pPath);
Stack<TreeNode> qPath = new Stack<>();
nodePath(root,q,qPath);
int pSize = pPath.size();
int qSize = qPath.size();
while (pSize > 0 && qSize > 0){
if(pSize > qSize){
pPath.pop();
pSize--;
}else if(pSize < qSize){
qPath.pop();
qSize--;
}else{
if(pPath.peek() == qPath.peek()){
return pPath.peek();
}else{
pPath.pop();
qPath.pop();
pSize--;
qSize--;
}
}
}
return null;
}
// 先获取到p,q结点的路径
public boolean nodePath(TreeNode root, TreeNode node, Stack<TreeNode> path){
if(null == root){
// 树为空返回null
return false;
}
// 将当前结点入栈
path.push(root);
if(node == root){
// 如果当前遍历到的结点root就是最终结点node
// root因为不断递归所以root不是最开始的根节点了,而是遍历到的当前结点
return true;
}
if(nodePath(root.left,node,path) || nodePath(root.right,node,path)){
// 在root的左子树中找node没找到就在右子树中找,找到了返回true
return true;
}
// 没在树中找到node,所以之前压栈的结点要删掉
path.pop();
return false;
}
}
方法二:(从第二种情况二叉搜索树当中得到启发)
p,q分别在root的左右子树中–》最近的公共祖先就是root
p,q在root的左子树中–》递归到根节点的左子树中找
p,q在root的右子树中–》递归到根节点的右子树中找
那么如何知道p在根的哪个子树中–》可以转成检测节点是否在一棵树中的题
看到底是在左右子树只需要改变传参,传参是左子树的节点就是查找是否是在左子树中,传参是右子树的节点就是查找是否是在右子树中
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(null == root || null == p || null == q){
return root;
}
if(root == p || root == q){return root;}
boolean pInLeft = false;
boolean pInRight = false;
boolean qInLeft = false;
boolean qInRight = false;
// 检测p在左子树还是右子树
if(isInTree(root.right,p)){
// p在右子树中
pInRight = true;
} else{
pInLeft = true;
}
// 检测q在左子树还是右子树
if(isInTree(root.right,q)){
// p在右子树中
qInRight = true;
} else{
qInLeft = true;
}
if((pInLeft && qInRight) || (pInRight && qInLeft)) {
// p,q分别在root的左右子树中--》最近的公共祖先就是root
return root;
} else if(pInLeft && qInLeft) {
// p,q在root的左子树中--》递归到根节点的左子树中找
return lowestCommonAncestor(root.left,p,q);
} else {
// p,q在root的右子树中--》递归到根节点的右子树中找
return lowestCommonAncestor(root.right,p,q);
}
}
private boolean isInTree(TreeNode root,TreeNode node){
if(root == null){
return false;
}
if(root == node){
return true;
}
if(isInTree(root.left,node) || isInTree(root.right,node)){
return true;
}
return false;
}
}