写在前面
如果觉得写得好或者有所收获,记得点个关注和点个赞,不胜感激。
我之前写过一篇文章,叫做二叉树的相关算法集合,其实现在回过头来,感觉标题太大言不惭了,应该改成遍历算法集合,因为像今天遇到的这个问题,其实也是二叉树的相关算法,虽然解答的时候,实现了一种算法,但是,其实还有其他的一种算法思路,而且简洁明了,很值得学习,这里就记录讲解。
二叉搜索树的公共最近祖先问题
首先在正式讲解之前,我们先来讨论二叉搜索树的问题的解决方案。其实如果问题换成二叉搜索树的话,那么问题就简单很多了。二叉搜索树的性质如下:
- 节点 N 左子树上的所有节点的值都小于等于节点 N 的值
- 节点 N 右子树上的所有节点的值都大于等于节点 N 的值
- 左子树和右子树也都是 二叉搜索树(BST)
所以,我们可以通过比较各节点之间的值,来判断最近公共祖先的节点。而最近公共祖先节点有如下三种情况:
我们发现,这三种情况其实很好解决,非常容易想到的就是通过递归来完成我们的方案。具体的算法思路如下:
- 从根节点开始遍历树
- 如果节点 p 和节点 q 都在右子树上,那么以右孩子为根节点继续 1 的操作
- 如果节点 p 和节点 q 都在左子树上,那么以左孩子为根节点继续 1 的操作
- 如果条件 2 和条件 3 都不成立,这就意味着我们已经找到节 p 和节点 q 的 LCA 了
有了上面的算法思路,我们很容易写出代码
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}else if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}else return root;
}
当然,可以使用迭代来代替递归的解法,这里就不做深入的叙述了,有兴趣的可以自己去实现以下,我们主要来讲解更一般的情况,也就是二叉树的最近祖先问题。
二叉树的公共最近祖先问题
更一般的情况,也就是二叉树。对于普通的二叉树,我们其实第一直觉想到的就是,就是同时找到两个节点的路径,然后两条路
径重合的分开处的节点,就是我们要找的最近公共祖先节点了。要完成这样的思路,我们可以非常暴力的通过递归,遍历记录两个节点的路径。不过我最开始想到的思路,是在这种暴力的基础上进行改进,可以简单的理解成剪枝了一下(直觉让我觉得直接暴力不是我想要的,嘿嘿嘿)。所以我就是用了类似前缀的思路,点到为止。我是通过一个哈希表,Key 为子节点,Value是节点,这样就可以记录路径关系,而不是简单的使用List 来记录(当然,使用List记录也可以,不过这样的话,还是需要使用递归来完成,所以我想使用迭代)。算法思路如下:
- 从根节点开始遍历树。
- 通过循环迭代,在找到 p 和 q 之前,将父指针存储在字典中。
- 一旦我们找到了 p 和 q,我们就可以使用父指针字典获得 p 的所有祖先,并添加到一个称为祖先的集合中。
- 同样,我们遍历节点 q 的祖先。如果祖先存在于为 p 设置的祖先中,这意味着这是 p 和 q 之间的第一个公共祖先(同时向上遍历),因此这是 LCA 节点。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Deque<TreeNode> stack = new ArrayDeque<>();
Map<TreeNode, TreeNode> parent = new HashMap<>();
parent.put(root, null);
stack.push(root);
//通过一层一层的遍历,知道遍历到两个节点为止
while (!parent.containsKey(p) || !parent.containsKey(q)) {
TreeNode node = stack.pop();
if (node.left != null) {
parent.put(node.left, node);
stack.push(node.left);
}
if (node.right != null) {
parent.put(node.right, node);
stack.push(node.right);
}
}
Set<TreeNode> ancestors = new HashSet<>();
while (p != null) {
ancestors.add(p);
p = parent.get(p);
}
while (!ancestors.contains(q))
q = parent.get(q);
return q;
}
不记录路径思路
当然,我们上面的第一直觉就是我们通过记录两个节点的路径,再来找公共祖先,但是其实我们在遍历的过程中,就可以通过记录当前祖先的方式,找到公共祖先。不过和上面的思路的遍历方式区别的地方在于,上面的思路是通过层次遍历,而这种方式是通过深度遍历。可能这样说比较抽象,我们这里使用图来理解,图是直接引用LeetCode的图。
我们通过栈来实现深度遍历,当我们遇到第一个节点的时候,我们就记录该节点为公共祖先节点,然后继续遍历。
当记录的公共祖先节点被弹出栈时,我们就要把公共节点移动到当前栈顶的节点,如图所示。
继续弹出
当遇到第二个节点的时候,公共祖先的节点就被找到了。
private static int BOTH_PENDING = 2;
private static int LEFT_DONE = 1;
private static int BOTH_DONE = 0;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Stack<Pair<TreeNode, Integer>> stack = new Stack<Pair<TreeNode, Integer>>();
stack.push(new Pair<TreeNode, Integer>(root, Solution.BOTH_PENDING));
boolean one_node_found = false;
TreeNode LCA = null;
TreeNode child_node = null;
while (!stack.isEmpty()) {
Pair<TreeNode, Integer> top = stack.peek();
TreeNode parent_node = top.getKey();
int parent_state = top.getValue();
if (parent_state != Solution.BOTH_DONE) {
if (parent_state == Solution.BOTH_PENDING) {
if (parent_node == p || parent_node == q) {
if (one_node_found) {
return LCA;
} else {
one_node_found = true;
LCA = stack.peek().getKey();
}
}
child_node = parent_node.left;
} else {
child_node = parent_node.right;
}
stack.pop();
stack.push(new Pair<TreeNode, Integer>(parent_node, parent_state - 1));
if (child_node != null) {
stack.push(new Pair<TreeNode, Integer>(child_node, Solution.BOTH_PENDING));
}
} else {
if (LCA == stack.pop().getKey() && one_node_found) {
LCA = stack.peek().getKey();
}
}
}
return null;
}
递归思路
其实这里为啥把递归放最后,是因为这里要讲的递归思路的一种巧妙方式,我觉得很值得学习。
- 从根节点开始遍历树。
- 如果当前节点本身是 p 或 q 中的一个,我们会将变量 mid 标记为 true,并继续搜索左右分支中的另一个节点。
- 如果左分支或右分支中的任何一个返回 true,则表示在下面找到了两个节点中的一个。
- 如果在遍历的任何点上,left、right 或者 mid 三个标记中的任意两个变为 true,这意味着我们找到了节点 p 和 q 的最近公共祖先。
private TreeNode res;
public boolean recurseTree(TreeNode currentNode, TreeNode p, TreeNode q) {
if (currentNode == null) return false;
int left = recurseTree(currentNode.left, p, q) ? 1 : 0;
int right = recurseTree(currentNode.right, p, q) ? 1 : 0;
int mid = 0;
if (currentNode.val == p.val || currentNode.val == q.val) mid = 1;
if (left + right + mid >= 2) {
res = currentNode;
return false;
} else if (left + right + mid > 0) {
return true;
} else return false;
}
public TreeNode lowestCommonAncestorS1(TreeNode root, TreeNode p, TreeNode q) {
recurseTree(root, p, q);
return res;
}