1. 程式人生 > 實用技巧 >劍指Offer_#7_重建二叉樹

劍指Offer_#7_重建二叉樹

劍指Offer_#7_重建二叉樹

Contents

題目

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。
例如,給出

前序遍歷 preorder =[3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]

返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

限制:
0 <= 節點個數 <= 5000

思路分析

整體思路

引用題解區liweiwei大佬的一個圖解 題解連結

這個圖片很清晰地展示了本題的思路。
整體的思路如下:

  1. 找到當前樹的根節點。根節點一定是前序遍歷序列的第一個,即上圖中的1。
  2. 找到根節點在中序遍歷序列當中的位置,即上圖中pivot指向的位置。
  3. 根據中序遍歷中根節點的位置,可以將整個inorder陣列劃分為左子樹部分和右子樹部分,即綠色框和紅色框部分。
  4. 根據中序遍歷中左子樹部分的個數,反過來又可以把preorder陣列劃分為左子樹部分和右子樹部分,即綠色框和紅色框部分。

以上的步驟將前序遍歷序列和中序遍歷序列劃分成為3部分

  • 根節點
  • 左子樹
  • 右子樹

但是這還不足以重構整個二叉樹,因為這是個遞迴問題,還需要解決遞迴子問題

  • 左子樹部分還可以劃分為左子樹的左子樹,左子樹的右子樹
  • 右子樹部分還可以劃分為右子樹的左子樹,右子樹的右子樹

我們需要編寫遞迴函式來實現上述邏輯。

遞迴函式

遞迴引數(函式簽名)
TreeNode recur(int preL,int preR,int inL,int inR)

  • preL,preR表示當前子樹在前序遍歷序列preorder當中的左右邊界
  • inL,inR表示當前子樹在中序遍歷序列inorder當中的左右邊界

遞迴終止條件
如果左邊界大於右邊界,表示當前的子樹沒有任何節點,是null
if(preL > preR || inL > inR) return null;

遞推過程

  1. 構建當前子樹的根節點root,root的值是preorder[preL]
  2. 在中序遍歷序列inorder
    中尋找根節點root的值所在的位置
    • 方法1:提前構建一個HashMap,儲存鍵值對<inorder[i],i>,可以直接查詢到
    • 方法2:遍歷inorder陣列,找到root的值
  3. 根據上面的圖解,我們可以劃分出root的左右子樹在兩個序列裡的範圍。得到root的左右子樹的preL,preR,inL,inR。呼叫遞迴子函式,構造出root的左右子樹。

返回值(回溯)
返回當前構建的root子樹,成為更高一層遞迴函式中的左右子樹。

其他細節

特殊輸入

  • preorder/inorder是null
  • preorder/inorder長度為0
  • preorderinorder長度不同

以上情況都無法重建出一個二叉樹,返回null

全域性變數

  • hashMap變數,用於儲存鍵值對<inorder[i],i>
  • po變數,儲存preorder陣列,避免遞迴函式引數太多

解答

class Solution {
    HashMap<Integer,Integer> map = new HashMap();
    //前序遍歷序列的全域性變數,避免遞迴函式的引數太多
    int[] po;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int preLen = preorder.length;
        int inLen = inorder.length;
        //特殊輸入:null,空陣列,兩陣列長度不同
        if(preorder == null || inorder == null || preLen == 0 || inLen == 0 || preLen != inLen)
            return null;
        po = preorder;
        //將中序遍歷序列的<inorder[i],i>存入map,以便在inorder陣列中快速找到子樹根節點的值
        for(int i = 0;i <= inorder.length - 1;i++){
            map.put(inorder[i],i);
        }
        //開啟遞迴呼叫
        return recur(0,preLen - 1,0,inLen - 1);
    }
    //遞迴函式引數:
    //preL,preR表示當前子樹在前序遍歷序列preorder當中的左右邊界
    //inL,inR表示當前子樹在中序遍歷序列inorder當中的左右邊界
    private TreeNode recur(int preL,int preR,int inL,int inR){
        //遞迴出口條件:左邊界大於右邊界,含義是當前子樹為空
        if(preL > preR || inL > inR) return null;
        //當前子樹的根節點的值就是po[preL]
        int pivot = po[preL];
        TreeNode root = new TreeNode(pivot);
        //利用map,找到當前子樹根節點在中序遍歷序列inorder中的索引
        int pivotIndex = map.get(pivot);
        //開啟下一級遞迴呼叫,將程式阻塞在這裡,直到滿足遞迴終止條件
        //重點在於遞推過程中的4個引數,必須在紙上先畫出圖,推匯出引數,再寫程式碼
        root.left = recur(preL + 1,preL + pivotIndex - inL,inL,pivotIndex - 1);
        root.right = recur(preL + pivotIndex - inL + 1,preR,pivotIndex + 1,inR);
        //回溯,將構造好的子樹返回給上一級遞迴函式
        return root;
    }
}

複雜度分析

時間複雜度:O(n)

  • 初始化hashmap,複雜度是O(n)
  • 每個遞迴函式構建一個節點,所以遞迴函式呼叫O(n)

空間複雜度:O(n)

  • hashmap佔用空間O(n)
  • 遞迴呼叫佔用空間O(n)