LeetCode 第236题 二叉树的最近公共祖先 做题记录
题目描述
我的解法
思路
- 遍历过程中记录节点p、q的所有祖先节点(按深度从小到大按顺序添加)
cur.left/cur.right 就代表着这父子关系,可以将cur加入到记录的链结构中,同时涉及到回溯。
(1)参数:访问节点,p或q,祖先列表 ;返回值:boolean
(2)终止条件:当节点为null时,返回false,当节点为q或p时,返回ture
(3)将当前节点加入祖先列表,对其左节点和右节点进行递归。递归结束后进行一次回溯,将祖先列表尾部的一个节点弹出。
- 获取到两个节点的祖先顺序表后,只要比遍历寻找最小的一个就可以了
对应Java代码
class Solution {
public List<TreeNode> ancestor = new LinkedList<TreeNode>();
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(p == null || q == null) {return null;}
traversal(root, p);
TreeNode[] ancestorP = new TreeNode[ancestor.size()];
for (int i = 0; i < ancestorP.length; i++) {
ancestorP[i] = ancestor.get(i);
}
ancestor.clear();
traversal(root, q);
TreeNode[] ancestorQ = new TreeNode[ancestor.size()];
for (int i = 0; i < ancestorQ.length; i++) {
ancestorQ[i] = ancestor.get(i);
}
for(int i = ancestorP.length - 1; i >= 0 ; i--){
for(int j = ancestorQ.length - 1; j >= 0 ; j--){
if(ancestorP[i].val == ancestorQ[j].val){
return ancestorP[i];
}
}
}
return null;
}
public boolean traversal(TreeNode cur, TreeNode target){
// 1.修改处1
ancestor.add(cur);
if(cur == null) {return false;}
if(cur.val == target.val) {return true;}
if(traversal(cur.left, target) == true) {return true;}
ancestor.remove(ancestor.size() - 1);
if(traversal(cur.right, target) == true) {return true;}
ancestor.remove(ancestor.size() - 1);
return false;
}
}
编码过程中主要解决的几个问题
- 递归过程中使用ancestor.remove(ancestor.size() - 1);回溯时没有考虑到空节点提前退出的情况,这时候没有节点入队列依旧进行了回溯。
修改方法就是在进行终结条件判断前把节点入队列(即使是null) - ancestorP和ancestorQ一开始使用了列表的浅拷贝
List<TreeNode> ancestorP = ancestor
这种实际上是赋值了引用,导致ancestor修改后,P和Q会同时变化,两者总是一致的。故用数组的方式进行存储。
复杂度分析
时间复杂度:O(n²) 对树进行了两次遍历,每次时间复杂度都是O(n) 但是后续对两个数组查找公共值的情况,最坏情况是O(n²)
空间复杂度:O(n)
更优解法
此部分转载于公众号代码随想录
思路
-
二叉树如何自底向上查找呢?
遇到这个题目首先想到的是能自底向上查就好了,这样就能找到公共祖先了。
而回溯的过程就是从低到上,后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。 -
如何判断一个节点是节点q和节点p的公共祖先呢?
如果找到一个节点,发现左子树出现节点p,右子树出现节点q,或者相反,那么该节点就是节点p和q的最近公共祖先。
递归三部曲:
- 确定递归函数返回值以及参数
需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 - 确定终止条件
如果找到了 节点p或者q,或者遇到空节点,就返回。 - 确定单层递归逻辑
值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。在二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值!如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?
搜索一条边的写法:
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;
搜索整个树写法;
left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的逻辑处理;
而本题对left和right的逻辑处理:
1.「如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解」
2.「如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然」。
完整流程图如下:
对应Java代码
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;}
else if(left != null && right == null){return left;}
return right;
}
}
总结
「那么我给大家归纳如下三点」:
- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。
- 在回溯的过程中,必然要遍历整颗二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
- 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。