1. 程式人生 > 實用技巧 >劍指Offer_#68-II_二叉樹的最近公共祖先

劍指Offer_#68-II_二叉樹的最近公共祖先

劍指Offer_#68-II_二叉樹的最近公共祖先

劍指offer

Contents

題目

給定一個二叉樹, 找到該樹中兩個指定節點的最近公共祖先。
百度百科中最近公共祖先的定義為:“對於有根樹 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 為不同節點且均存在於給定的二叉樹中。

思路分析

最近公共祖先

最近公共祖先只有三種情況:

  1. p 和 q在 root的子樹中,且分列 root 的 異側(即分別在左、右子樹中);
  2. p = root,且 q 在 root的左或右子樹中;
  3. 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
    代表找到了p,q
  • root == null代表遍歷到葉節點也沒找到p,q,返回null

遞推過程

  1. 開啟左子樹遞迴,也就是在左子樹繼續尋找p,q,並記錄返回值left
  2. 開啟右子樹遞迴,也就是在右子樹繼續尋找p,q,並記錄返回值right
  • 返回值不是null,代表的就是p,q的公共祖先
  • 返回值是null,表示這個子樹裡沒有p,q的公共祖先

返回值

返回值表示的是最近公共祖先,沒找到最近公共祖先時,返回值都是null;找到了最近公共祖先後,會將這個節點回溯到二叉樹根節點,作為最後的返回值。

  1. left和right都為空,說明當前節點的左右子樹裡找不到p,q的公共祖先,那麼返回null
  2. left和right都不為空,說明p,q就分別在當前節點的左右子樹當中,當前節點就是公共祖先
  3. 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)