题目描述
给定一颗二叉树,以及2个指定节点p
, q
,找到这2个节点的最近公共祖先。注意,一个节点也可以是它自己的祖先。并且根据提示,有p != q
,并且p
和q
均存在于二叉树中。
递归
由于p
和q
一定存在于树中,则其最近公共祖先一定存在。其最近公共祖先有如下2种情况
- 最近公共祖先是
p
或者q
- 最近公共祖先不是
p
或者q
对于情况1,假设最近公共祖先是p
,则q
一定存在于p
的左子树或右子树,此时直接返回p
即可。当最近公共祖先是q
时同理。
对于情况2,假设最近公共祖先是r
,则p
和q
一定分别位于r
的左子树和右子树上。
由于求解的是这种节点的父子关系,很容易想到需要用DFS+回溯的方式。我们期望在DFS的过程中,能够把节点p
和q
的信息,通过回溯传递上来。
所以我们这样来定义递归函数:
用dfs(node)
这个函数表示,对以node
为根节点的二叉树进行搜索,若p
在树中出现,则返回p
;若q
在树中出现,则返回q
;若p
和q
同时在树中出现,则返回p
和q
的最近公共祖先。
伪代码为:
dfs(node):
if (node == null) return null; // 跨过叶子节点
if (node == p || node == q) return node; // 找到p或者q
left = dfs(node.left); // 查找node的左子树, 看p或q是否出现
right = dfs(node.right); // 查找node的右子树, 看p或q是否出现
if (left != null && right != null) return node; // 当 p 和 q 分别在 node 的左子树和右子树出现, 则最近公共祖先就是 node
if (left != null) return left; // 若左子树上找到了p或q, 返回左子树的查找结果
return right; // 右子树上找到了p或q,返回右子树的查找结果
假设最近公共祖先不是p
或q
,是r
。并且假设p
位于r
左子树,q
位于r
右子树。则通过对r.left
进行dfs搜索,能够把p
的节点信息传递上来,对r.right
进行dfs搜索,能够把q
的节点信息传递上来。并且r
节点,是唯一一个对其左子树进行dfs有结果,并且对其右子树进行dfs也有结果的节点。(r
节点下面的节点,最多只可能在某一侧的dfs中有结果;r
节点上面的节点,只会在r
所在的这一侧的子树中,有dfs的结果)。
假设最近公共祖先是p
或q
,则同样在遇到p
或q
时,dfs函数就提前返回了,和本题的答案相一致。
注意该方法中,对递归函数dfs
的定义,其实是带有扩展的,它求解的是:
- 若
p
出现在二叉树中,则返回p
- 若
q
出现,则返回q
- 若
p
和q
同时出现,则返回二者的最近公共祖先 - 若
p
和q
都不出现,返回null
而由于本题中p
和q
一定存在于二叉树中,所以该方法能求出正确结果。
dfs
函数在整个搜索过程中的作用是,将p
或q
的信息,通过返回值,传递上来(从子节点,往上传递给父节点,回溯)。
代码如下:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || 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;
return left != null ? left : right;
}
}
哈希表
还有一种思路,由于我们要找的是公共祖先。那么我们可以先遍历一次,把所有节点的父节点存下来。然后对p
和q
,依次往上寻找其父节点,把访问过的父节点存起来。先把p
往上遍历,遍历完。随后开始遍历q
,若在遍历q
的父节点的过程中,出现了某个已经访问过的节点,则这个节点就是p
和q
的公共祖先,且是最近的,因为是从下往上遍历。
获得了所有节点的父节点后,其实这道题就变成了 160. 相交链表 这道题了。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
dfs(root, parent);
// 相交链表
Set<TreeNode> visited = new HashSet<>();
while (p != null) {
visited.add(p);
p = parent.get(p);
}
while (q != null) {
if (visited.contains(q)) return q;
q = parent.get(q);
}
return null;
}
private void dfs(TreeNode root, Map<TreeNode, TreeNode> parent) {
if (root == null) return;
if (root.left != null) parent.put(root.left, root);
if (root.right != null) parent.put(root.right, root);
dfs(root.left, parent);
dfs(root.right, parent);
}
}
按相交链表这道题的一种巧妙的思路来做,如下
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
dfs(root, parent);
// 相交链表
TreeNode curP = p, curQ = q;
while (curP != curQ) {
curP = parent.get(curP) == null ? q : parent.get(curP);
curQ = parent.get(curQ) == null ? p : parent.get(curQ);
}
return curP;
}
private void dfs(TreeNode root, Map<TreeNode, TreeNode> parent) {
if (root == null) return;
if (root.left != null) parent.put(root.left, root);
if (root.right != null) parent.put(root.right, root);
dfs(root.left, parent);
dfs(root.right, parent);
}
}
扩展
假设p
和q
不一定都存在于二叉树当中,那么上面的方法就无法求出正确结果了。
我们换一种思路,我们设一个外部变量ans
,来保存在dfs
过程中可能搜到的答案,并且这样定义dfs
函数:
dfs(node)
表示:在以node
为根节点的二叉树中进行搜索,若树中出现了p
或者q
,则返回true
,否则返回false
。
我们从根节点root
开始进行DFS:
- 当
root
等于p
或q
时,只需要递归的搜索root
的左子树和右子树,只要其中一侧的dfs
返回了true
,则说明找到了另一个节点,此时更新ans = root
,然后提前返回即可(最近公共祖先是p
或者q
的情况) - 当
root
不等于p
或q
时,递归搜索两侧,只有当两侧的dfs
都返回了true
时,更新ans = root
(最近公共祖先不是p
或者q
的情况,此时的root
节点,是唯一一个对其左右子树调用dfs
都返回true
的节点) - 当
ans != null
时,说明已经找到答案,直接提前返回
另外要注意的是,假设root = p
,且对root
的子树调用dfs
返回true
,则说明在root
子树中找到的一定是节点q
;类似地,假设root
不为p
也不为q
,若对root
的左子树和右子树分别调用dfs
都返回true
,则一定是p
和q
分别位于root
的左右子树当中。
也就是说,当找到了一个节点时,对另外一边调用dfs
如果返回true
,则一定是找到了另外一个节点。(同一个节点不可能存在于一棵树上的两个位置)
这种解法能够在p
或 q
不存在于二叉树的情况下,返回正确结果(null
)。
class Solution {
TreeNode ans = null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root, p, q);
return ans;
}
private boolean dfs(TreeNode cur, TreeNode p, TreeNode q) {
if (cur == null || ans != null) return false; // 跨过叶子节点, 或者已经找到答案, 直接返回
if (cur == p || cur == q) {
if (dfs(cur.left, p, q) || dfs(cur.right, p, q)) {
ans = cur;
}
return true;
}
boolean left = dfs(cur.left, p, q);
boolean right = dfs(cur.right, p, q);
if (left && right) {
ans = cur;
}
return left || right;
}
}