劍指Offer_#68-II_二叉樹的最近公共祖先
阿新 • • 發佈:2020-07-26
劍指Offer_#68-II_二叉樹的最近公共祖先
劍指offerContents
題目
給定一個二叉樹, 找到該樹中兩個指定節點的最近公共祖先。
百度百科中最近公共祖先的定義為:“對於有根樹 T 的兩個結點 p、q,最近公共祖先表示為一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度儘可能大(一個節點也可以是它自己的祖先)。”
例如,給定如下二叉樹: root =[3,5,1,6,2,0,8,null,null,7,4]
示例 1:
輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
輸出: 3
解釋: 節點 5 和節點 1 的最近公共祖先是節點 3。
示例2:
輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
輸出: 5
解釋: 節點 5 和節點 4 的最近公共祖先是節點 5。因為根據定義最近公共祖先節點可以為節點本身。
說明:
所有節點的值都是唯一的。
p、q 為不同節點且均存在於給定的二叉樹中。
思路分析
最近公共祖先
最近公共祖先只有三種情況:
- p 和 q在 root的子樹中,且分列 root 的 異側(即分別在左、右子樹中);
- p = root,且 q 在 root的左或右子樹中;
- q = root ,且 p在 root 的左或右子樹中;
排除了上述情況之後,只剩下一種情況,即p 和 q在 root的子樹中,且都在 root 的 同側(即都在左子樹種或都在右子樹中),此時root必然不是最近的祖先,因為root下方肯定還有更近的祖先。
思路1:遞迴後序遍歷
為什麼是後續遍歷?因為一個節點是否是p,q的公共祖先,必須先看其左右子樹當中是否包含p,q。
終止條件
root == null || root == p || root == q
,直接返回root。
root == p || root == q
root == null
代表遍歷到葉節點也沒找到p,q,返回null
遞推過程
- 開啟左子樹遞迴,也就是在左子樹繼續尋找p,q,並記錄返回值left
- 開啟右子樹遞迴,也就是在右子樹繼續尋找p,q,並記錄返回值right
- 返回值不是null,代表的就是p,q的公共祖先
- 返回值是null,表示這個子樹裡沒有p,q的公共祖先
返回值
返回值表示的是最近公共祖先,沒找到最近公共祖先時,返回值都是null;找到了最近公共祖先後,會將這個節點回溯到二叉樹根節點,作為最後的返回值。
- left和right都為空,說明當前節點的左右子樹裡找不到p,q的公共祖先,那麼返回null
- left和right都不為空,說明p,q就分別在當前節點的左右子樹當中,當前節點就是公共祖先
- left和right有一個為空,另一個非空,說明p,q只可能在非空的那個子樹當中,返回非空的子樹根節點
思路2:到p,q的路徑的最後共同節點
這是書上的解法,首先找到以根節點開始,以p,q結尾的兩條路徑。然後找到兩條路徑公共部分的最後一個節點,就是最近的公共祖先節點。如下圖。
尋找路徑採用前序遍歷+回溯的方法。
解答
解答1:遞迴後序遍歷
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
//找左子樹當中的p或q,儲存到left當中
TreeNode left = lowestCommonAncestor(root.left, p, q);
//找右子樹當中的p或q,儲存到right當中
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null && right == null) return null;
//如果left和right之一為null,那麼最近公共祖先只能是在非null那個子樹當中
if(left == null) return right;
if(right == null) return left;
//根據root左子樹和右子樹的搜尋結果,判斷root是否是p,q的公共祖先
//如果left和right非空,說明root就是最近的公共祖先
return root;
}
}
複雜度分析
時間複雜度O(n),最多需要遍歷所有節點
空間複雜度O(n),最差的時候,需要開啟n層遞迴,佔用的棧空間是O(n)
解答2:到p,q的路徑的最後共同節點
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
List<TreeNode> path1 = new ArrayList<>();
List<TreeNode> path2 = new ArrayList<>();
getPath(root,p,path1);
getPath(root,q,path2);
TreeNode res = null;
//因為res在公共路徑上,所以只需要取較小的路徑長度即可
int n = Math.min(path1.size(),path2.size());
//公共路徑最後一個節點就是最近的公共祖先
for(int i = 0;i < n;i++){
if(path1.get(i) == path2.get(i)) res = path1.get(i);
}
return res;
}
//前序遍歷搜尋p,q,儲存路徑
void getPath(TreeNode root,TreeNode node,List<TreeNode> path){
if(root == null) return;
path.add(root);
if(root == node) return;
//為什麼要重複寫if語句?因為getPath()之後path就變化了,需要重新判斷
if(path.get(path.size()-1)!=node){
getPath(root.left,node,path);
}
if(path.get(path.size()-1)!=node){
getPath(root.right,node,path);
}
if(path.get(path.size()-1)!=node){
path.remove(path.size()-1);
}
}
}
複雜度分析
時間複雜度:O(n),需要遍歷兩次樹,得到到p和到q的節點,每一次是O(n),加起來也是O(n)
空間複雜度:使用path1,path2儲存路徑,最差情況是O(n),一般情況是O(logn)