最近公共祖先
题目
题目链接: 二叉树的最近公共祖先
思路讲解一
我们的第一个方法就是简单的分类讨论, 我们首先来看下面当最近公共祖先为根节点的情况
此时我们可以得到两种情况:
- 当左右子树都没有最近公共祖先时/两个节点位于根节点左右两侧时, 最近公共祖先在根节点上
- 当有一个节点位于根节点时, 最近公共祖先在根节点上
然后我们来看如果最近公共祖先不是根节点的情况
实际上, 这两个情况本质上是可以看作同一种情况的, 也就是: 当两个节点位于同一个子树上时, 最近公共祖先在此子树上面.
并且对于最近公共祖先位于子树上面的情况, 我们可以通过递归将其转换为前面的两个情况, 如下
那么此时我们就可以确定, 这个题目是可以通过递归来实现的, 因为向下递归后子问题的情况和主问题一致.
总结一下上面的思路讲解:
- 当两个节点位于根两侧时, 最近公共祖先为根节点
- 当两个节点位于同侧时, 最近公共祖先处于同侧子树上
- 当有一个节点位于根时, 最近公共祖先为根节点
我们就依旧是二叉树的深搜套路. 先根据题目要求,设定函数返回值为返回两个节点的最近公共祖先, 但是此时我们只需要简单的进行分析, 就会发现这个返回值根本不够用. 因为如果我们设定返回最近公共祖先, 那么下面的这两个情况我们根本就无法区分了
也就是说, 我们的返回值设定, 实际上是有问题的. 换句话说, 返回的信息应该还需要能够代表一些信息, 从而能够区分上面这样的情况. 并且这个返回值应该提供能够让我去确定, 当前节点是否是一个最近公共祖先的信息.
那么除了让返回值能够代表最近公共祖先以外, 还有什么方式可以确定最近公共祖先呢? 此时我们可以会想到我们最开始的分析, 我们是如何分析出最近公共祖先的位置的呢? 答案就是根据 p 和 q 节点的位置, 那么此时我们就可以设定返回值为 p 节点或 q 节点的存在情况.
那么此时这个返回值就非常的巧妙了, 我们来看看为什么. (下面是对返回值正确性的一些说明, 如果你没有疑问, 可以直接到最下方的深搜分析)
经过我们最开始的分析可以发现, p 和 q 要么这个节点在左右两侧, 要么一个在根, 要么两个同侧.
我们先看左右两侧的情况, 如果是左右两侧, 那么在最近公共祖先位置, 左右的返回值肯定不会是空, 这个时候就可以认定这个位置是最近公共祖先了.
此时可能有人要问了: 那此时你向上递归, 有没有可能会改变你的这个祖先的值呢?
实际上是不可能的, 因为我们的返回值是 p 和 q 的存在情况, 当我们确认公共祖先后, 证明 p 和 q 一定在刚刚那个公共祖先的位置以及它的下面, 你往上递归, 是不可能遇到其他的 p 和 q 的存在情况的.
接下来就是第二个, 一个节点在根的情况.
此时可能有人要问了: 那么如果 p 或者 q 节点在根, 那此时我们应该是直接返回当前节点的存在情况, 那么这样如果 q 在下面是否会被忽略呢?
想必问出这样问题的人, 应该是纠结于下面的这个情况, 因此我们进行分析
此时直接返回会有问题吗? 答案是, 也是不会的, 因为如果 q 在右边, 自然会像上面的情况一样, 在后面把信息传递过来, 如下图所示
如果没有传递过来 q 的位置信息, 此时就能够充分的证明, 这个 q 一定在我的下面, 而不是在我的对面. 因为如果出现在对面, 我一定是可以在上面的某个节点处获取到的. 这个间接的排除也是非常的巧妙的.
最后一个情况则是两个同侧的情况, 实际上递归下去就是上面两个情况, 如果还不理解, 可以多画几个图来自行理解.
那么我们就设定返回值为 p 和 q 节点的存在状况, 然后进行深搜的分析
- 根节点处理: 如果根节点等于 p 或者 q, 返回根节点
- 左子树处理: 递归左子树找 p 和 q
- 右子树处理: 递归右子树找 p 和 q
- 边界条件: 为空返回空, 没找到
- 返回值: 如果左右子树找到了, 那么返回当前根节点. 如果只有一边找到了, 返回找到的一边
代码书写一
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 边界条件
if(root == null) 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 && right != null){
return root;
}else {
return left != null ? left : right;
}
}
}
思路讲解二
看作反向的链表相交, 利用深搜 + 栈存储遍历路径, 随后通过栈来找到第一个相交节点. 如下图
深搜思路:
设定返回值为是否找到对应路径
- 处理根节点: 把根节点放入栈, 查看是否是目标节点, 是返回 true , 不是返回 false
- 处理左子树: 搜索左子树
- 处理右子树: 搜索右子树
- 边界条件: 如果为空 返回false
- 返回值: 如果左右子树中有一个找到了, 直接返回 true, 否则出栈返回 false
代码书写二
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Stack<TreeNode> s1 = new Stack<>();
Stack<TreeNode> s2 = new Stack<>();
getPath(root, p, s1);
getPath(root, q, s2);
// 将两个栈的节点数弄到相同
while (s1.size() > s2.size()) {
s1.pop();
}
while (s2.size() > s1.size()) {
s1.pop();
}
// 找到第一个相同节点
while (s1.peek() != s2.peek()) {
s1.pop();
s2.pop();
}
return s1.peek();
}
public boolean getPath(TreeNode root, TreeNode target, Stack<TreeNode> stack) {
// 边界条件
if (root == null) {
return false;
}
// 处理根节点
stack.push(root);
if (root == target) return true;
// 处理左子树
boolean left = getPath(root.left, target, stack);
// 返回值, 提前检测, 稍微减少一些开销
if (left) return true;
// 处理右子树
boolean right = getPath(root.right, target, stack);
// 返回值
if (right) return true;
// 都没有找到, 出栈, 返回false
stack.pop();
return false;
}
}