【劍指Offer】面試題07. 重建二叉樹

 

 

題目

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

例如,給出

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

返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

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

思路:遞歸

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

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

前序遍歷的第一個節點是根節點,只要找到根節點在中序遍歷中的位置,在根節點之前被訪問的節點都位於左子樹,在根節點之後被訪問的節點都位於右子樹,由此可知左子樹和右子樹分別有多少個節點。

由於樹中的節點數量與遍歷方式無關,通過中序遍歷得知左子樹和右子樹的節點數量之後,可以根據節點數量得到前序遍歷中的左子樹和右子樹的分界,因此可以進一步得到左子樹和右子樹各自的前序遍歷和中序遍歷,可以通過遞歸的方式,重建左子樹和右子樹,然後重建整個二叉樹。

使用一個 Map 存儲中序遍歷的每個元素及其對應的下標,目的是爲了快速獲得一個元素在中序遍歷中的位置。調用遞歸方法,對於前序遍歷和中序遍歷,下標範圍都是從 0 到 n-1,其中 n 是二叉樹節點個數。

遞歸方法的基準情形有兩個:判斷前序遍歷的下標範圍的開始和結束,若開始大於結束,則當前的二叉樹中沒有節點,返回空值 null。若開始等於結束,則當前的二叉樹中恰好有一個節點,根據節點值創建該節點作爲根節點並返回。

若開始小於結束,則當前的二叉樹中有多個節點。在中序遍歷中得到根節點的位置,從而得到左子樹和右子樹各自的下標範圍和節點數量,知道節點數量後,在前序遍歷中即可得到左子樹和右子樹各自的下標範圍,然後遞歸重建左子樹和右子樹,並將左右子樹的根節點分別作爲當前根節點的左右子節點。

代碼

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty()) return nullptr;
        return helper(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1);
    }

    TreeNode* helper(vector<int> &preorder, int pstart, int pend, vector<int> &inorder, int istart, int iend) {
        if (pend < pstart) return nullptr;
        int val = preorder[pstart];
        TreeNode* root = new TreeNode(val);
        auto it = find(inorder.begin() + istart, inorder.begin() + (iend + 1), val);//注意在[istart, iend]範圍內搜索
        int lenLeft = it - find(inorder.begin() + istart, inorder.begin() + (iend + 1), inorder[istart]);
        root->left = helper(preorder, pstart + 1, pstart + lenLeft, inorder, istart, istart + lenLeft - 1);
        root->right = helper(preorder, pstart + lenLeft + 1, pend, inorder, istart + lenLeft + 1, iend);
        return root;
    }
};

複雜度分析

  • 時間複雜度:O(n)O(n)。對於每個節點都有創建過程以及根據左右子樹重建過程。
  • 空間複雜度:O(n)O(n)。存儲整棵樹的開銷。

另一種寫法

修改求根節點索引。

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty()) return nullptr;
        return helper(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1);
    }

    TreeNode* helper(vector<int> &preorder, int pstart, int pend, vector<int> &inorder, int istart, int iend) {
        if (pend < pstart) return nullptr;
        int val = preorder[pstart];
        TreeNode* root = new TreeNode(val);
        auto it = find(inorder.begin() + istart, inorder.begin() + (iend + 1), val);
        int index = it - inorder.begin();        
        int lenLeft = index - istart;
        root->left = helper(preorder, pstart + 1, pstart + lenLeft, inorder, istart, index - 1);
        root->right = helper(preorder, pstart + lenLeft + 1, pend, inorder, index + 1, iend);
        return root;
    }
};

 

方法三:迭代
例如要重建的是如下二叉樹。

        3
       / \
      9  20
     /  /  \
    8  15   7
   / \
  5  10
 /
4
其前序遍歷和中序遍歷如下。

preorder = [3,9,8,5,4,10,20,15,7]
inorder = [4,5,8,10,9,3,15,20,7]
前序遍歷的第一個元素 3 是根節點,第二個元素 9 可能位於左子樹或者右子樹,需要通過中序遍歷判斷。

中序遍歷的第一個元素是 4 ,不是根節點 3,說明 9 位於左子樹,因爲根節點不是中序遍歷中的第一個節點。同理,前序遍歷的後幾個元素 8、5、4 也都位於左子樹,且每個節點都是其上一個節點的左子節點。

前序遍歷到元素 4,和中序遍歷的第一個元素相等,說明前序遍歷的下一個元素 10 位於右子樹。那麼 10 位於哪個元素的右子樹?從前序遍歷看,10 可能位於 4、5、8、9、3 這些元素中任何一個元素的右子樹。從中序遍歷看,10 在 8 的後面,因此 10 位於 8 的右子樹。把前序遍歷的順序反轉,則在 10 之前的元素是 4、5、8、9、3,其中 8 是最後一次相等的節點,因此前序遍歷的下一個元素位於中序遍歷中最後一次相等的節點的右子樹。

同理可知,20 位於 3 的右子樹,15 和 7 分別是 20 的左右子節點。

根據上述例子和分析,可以使用棧保存遍歷過的節點。初始時令中序遍歷的指針指向第一個元素,遍歷前序遍歷的數組,如果前序遍歷的元素不等於中序遍歷的指針指向的元素,則前序遍歷的元素爲上一個節點的左子節點。如果前序遍歷的元素等於中序遍歷的指針指向的元素,則正向遍歷中序遍歷的元素同時反向遍歷前序遍歷的元素,找到最後一次相等的元素,將前序遍歷的下一個節點作爲最後一次相等的元素的右子節點。其中,反向遍歷前序遍歷的元素可通過棧的彈出元素實現。

使用前序遍歷的第一個元素創建根節點。
創建一個棧,將根節點壓入棧內。
初始化中序遍歷下標爲 0。
遍歷前序遍歷的每個元素,判斷其上一個元素(即棧頂元素)是否等於中序遍歷下標指向的元素。
若上一個元素不等於中序遍歷下標指向的元素,則將當前元素作爲其上一個元素的左子節點,並將當前元素壓入棧內。
若上一個元素等於中序遍歷下標指向的元素,則從棧內彈出一個元素,同時令中序遍歷下標指向下一個元素,之後繼續判斷棧頂元素是否等於中序遍歷下標指向的元素,若相等則重複該操作,直至棧爲空或者元素不相等。然後令當前元素爲最後一個想等元素的右節點。
遍歷結束,返回根節點。

 

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