面試字節跳動時,我竟然遇到了 LeetCode 原題……

公衆號關注 “GitHubDaily”

設爲 “星標”,每天帶你逛 GitHub!

今天分享的題目來源於 LeetCode 上的劍指 Offer 系列 面試題07. 重建二叉樹,近半年在字節跳動算法面試環節出現過高達 14 次,你很可能遇到過,它屬於中高難度的算法題,今天吳師兄和你一起弄懂!

題目鏈接:https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-di-gui-fa-qin/

一、題目描述

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

例如,給出

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

返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

限制:

0 <= 節點個數 <= 5000

二、題目解析

首先,我們先來複習一下前序遍歷、中序遍歷。(在下方的視頻中分佈講解)

前序遍歷

二叉樹的前序遍歷順序是:根節點、左子樹、右子樹,每個子樹的遍歷順序同樣滿足前序遍歷順序。

中序遍歷

二叉樹的中序遍歷順序是:左子樹、根節點、右子樹,每個子樹的遍歷順序同樣滿足中序遍歷順序。

複習過後,我們可以得出以下結論:

  • 在二叉樹的 前序遍歷 序列中,第一個數字總是樹的根結點的值;

  • 在二叉樹的 中序遍歷 序列中,根結點的值在序列的中間,左子樹的結點的值位於根結點的值的左邊,而右子樹的結點的值位於根結點的值的右邊

以本題的序列爲例,前序遍歷序列的第一個數字 3 就是根結點的值,在中序遍歷序列,找到根結點值的位置。根據中序遍歷特點,在根結點的值 3 前面的數字都是左子樹結點的值,在根結點的值 3 後面的數字都是右子樹結點的值。

二叉樹很重要的一個性質是遞歸,在找到了左子樹、右子樹的前序遍歷序列和中序遍歷序列後,我們可以按照同樣的方法去確定 子左子樹 和 子右子樹 的構建。

具體的代碼編寫思路如下(來源於 Krahets's Blog):

  • 遞推參數: 前序遍歷中根節點的索引pre_root_idx、中序遍歷左邊界in_left_idx、中序遍歷右邊界in_right_idx

  • 終止條件: 當 in_left_idx &gt; in_right_idx ,子樹中序遍歷爲空,說明已經越過葉子節點,此時返回 null 。

  • 遞推工作:

  1. 建立根節點 root : 值爲前序遍歷中索引爲pre_root_idx的節點值。

  2. 搜索根節點 root 在中序遍歷的索引 i : 爲了提升搜索效率,本題解使用哈希表 map 預存儲中序遍歷的值與索引的映射關係,每次搜索的時間複雜度爲 O(1)。

  3. 構建根節點 root的左子樹和右子樹:通過調用  recursive()  方法開啓下一層遞歸。

  • 左子樹: 根節點索引爲 pre_root_idx + 1 ,中序遍歷的左右邊界分別爲 in_left_idx 和 i - 1。

  • 右子樹: 根節點索引爲 i - in_left_idx + pre_root_idx + 1(即:根節點索引 + 左子樹長度 + 1),中序遍歷的左右邊界分別爲 i + 1 和 in_right_idx。

  • 返回值: 返回 root,含義是當前遞歸層級建立的根節點 root 爲上一遞歸層級的根節點的左或右子節點。

三、動畫描述

四、圖片描述

五、參考代碼

class Solution {
    //在中序序列中查找與前序序列首結點相同元素的時候,如果使用 while 循環去一個個找效率很慢
    //這裏我們藉助數據結構 HashMap 來輔助查找,在開始遞歸之前把所有的中序序列的元素和它們所在的下標存到一個 map 中,這樣查找的時間複雜度是 O(logn)
    HashMap<Integer, Integer> map = new HashMap<>();

    //保留的前序遍歷
    int[] preorder;

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        //在開始遞歸之前把所有的中序序列的元素和它們所在的下標存到一個 map 中
        for (int i = 0; i < preorder.length; i++) {
            map.put(inorder[i], i);
        }
        //二叉樹的重要性質是遞歸
        return recursive(0,0,inorder.length-1);
    }

    /** 根據前序遍歷序列和中序遍歷序列重新組建二叉樹
     * @param pre_root_idx 前序遍歷的索引
     * @param in_left_idx  中序遍歷左邊界的索引
     * @param in_right_idx 中序遍歷右邊界的索引
     */
    public TreeNode recursive(int pre_root_idx, int in_left_idx, int in_right_idx) {

        //子樹中序遍歷爲空,說明已經越過葉子節點,此時返回 nul
        if (in_left_idx > in_right_idx) {
            return null;
        }

        //root_idx是在前序裏面的
        TreeNode root = new TreeNode(preorder[pre_root_idx]);

        // 通過 map ,根據前序的根節點的值,在中序中獲取當前根的索引
        int idx = map.get(preorder[pre_root_idx]);

        //左子樹的根節點就是 左子樹的(前序遍歷)第一個,就是 +1 ,左邊邊界就是 left ,右邊邊界是中間區分的idx-1
        root.left = recursive(pre_root_idx + 1, in_left_idx, idx - 1);

        //右子樹的根,就是右子樹(前序遍歷)的第一個,就是當前根節點 加上左子樹的數量
        root.right = recursive(pre_root_idx + (idx-1 - in_left_idx +1)  + 1, idx + 1, in_right_idx);

        return root;
    }
}

這段代碼的一個難點就是 root.left 與 root.right ,我這裏抽離出來詳細解釋一下。

1、root.left

2、root.right

六、複雜度分析

時間複雜度

時間複雜度爲 O(N)。

空間複雜度

空間複雜度爲 O(N)。

七、相關標籤

  • 遞歸

  • 哈希表

八、參考來源

  • 1、https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-di-gui-fa-qin/ 題解區

  • 2、https://krahets.gitee.io/views/sword-for-offer/2020-02-24-sword-for-offer-07.html

---

由 GitHubDaily 原班人馬打造的公衆號:GitCube,現已正式上線!
接下來我們將會在該公衆號上,爲大家分享優質的計算機學習資源與開發者工具,堅持每天一篇原創文章的輸出,感興趣的小夥伴可以關注一下哈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章