1. 程式人生 > 其它 >105.從前序遍歷與中序遍歷構建二叉樹

105.從前序遍歷與中序遍歷構建二叉樹

技術標籤:leetcode資料結構與演算法

構建二叉樹可以使用前序遍歷+中序,或者中序+後序,但是前序+後序無法構建,原因是前序遍歷列表中,第一個元素一定是根節點,剩餘的元素是左子樹的前序遍歷+右子樹的前序遍歷列表,即前序遍歷結構為: [ r o o t + [ l e f t + r i g h t ] ] [root+[left+right]] [root+[left+right]];而後序遍歷結構為: [ [ l e f t + r i g h t ] + r o o t ] [[left+right]+root] [[left+right]+root],如果給定前序遍歷和後序遍歷結果,僅能推斷出來的是誰是當前樹的根結點,誰是左子樹和右子樹的遍歷結果,但是無法區分左子樹與右子樹的遍歷序列。這也就是隻給定前序和後序無法構建二叉樹的原因。

題目連結

遞迴實現

思路這樣:可以將傳入的 p r e o r d e r preorder preorder i n o r d e r inorder inorder看作是一個子樹的遍歷結果,即當前我們所處理的僅僅是一棵子樹,這樣我們就可以置身於眾多遞迴過程中的一個。
前序遍歷的結構為:
[ r o o t + [ l e f t ] + [ r i g h t ] ] [root+[left]+[right]] [root+[left]+[right]]
中序遍歷的結構為:
[ [ l e f t ] + r o o t + [ r i g h t ] ] [[left]+root+[right]]

[[left]+root+[right]]
所以根據前序遍歷的第一個元素很容易定位其在中序遍歷中的位置,從而確定左子樹和右子樹的中序遍歷結果, r o o t root root在中序遍歷中的索引值其實也是左子樹的結點個數,所以很容易從前序遍歷序列中將左子樹和右子樹的前序遍歷序列分離開來。這樣我們就得到了左右子樹的前序遍歷和中序遍歷結果,接下來進行遞迴即可。

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if preorder ==
[]: return None core = preorder[0] inorder_core_index = inorder.index(core) root = TreeNode(val=core) # 從兩種遍歷中將左右子樹的元素全部分離開來 # # 這裡只有在樹中沒有重複元素的時候才可以 left = inorder[: inorder_core_index] right = inorder[inorder_core_index+1:] # inorder_core_index的數值也是左子樹的元素個數 # 前序遍歷和中序遍歷的共同點是,遍歷結果的最後一個元素一定是當前樹的右下角的葉子元素 root.left = self.buildTree(preorder[1: 1+inorder_core_index], left) root.right = self.buildTree(preorder[1+inorder_core_index:], right) return root

遞迴方法改進

容易看出來,遞迴演算法每層遞迴的時候,都需要查詢 r o o t root root i n o r d e r inorder inorder中的索引值,所以時間更多的花在了這裡,為了減少時間複雜度,可以使用雜湊表將 i n o r d e r inorder inorder中的結點索引資訊儲存起來,等到需要的時候直接查詢即可,時間複雜度降為 O ( 1 ) O(1) O(1),而儲存結點索引資訊所使用的記憶體與 i n o r d e r inorder inorder的長度有關,將上述思路實現為程式碼如下:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def mybuildTree(preorder, inorder):
            if preorder == []:
                return None
            
            core = preorder[0]
            inorder_core_index = index[core]
            root = TreeNode(val=core)
            # 從兩種遍歷中將左右子樹的元素全部分離開來
            #
            # 這裡只有在樹中沒有重複元素的時候才可以
            left = inorder[: inorder_core_index]
            right = inorder[inorder_core_index+1:]
            # inorder_core_index的數值也是左子樹的元素個數

            # 前序遍歷和中序遍歷的共同點是,遍歷結果的最後一個元素一定是當前樹的右下角的葉子元素
            root.left = mybuildTree(preorder[1: 1+inorder_core_index], left)
            root.right = mybuildTree(preorder[1+inorder_core_index:], right)
            return root

        # 構建雜湊對映表
        index = {val: ind for ind, val in enumerate(inorder)}
        return mybuildTree(preorder, inorder)

拿去執行,程式碼並不能跑通,問題出在哪裡?

沒有優化的時候,我們查詢索引直接用的是傳進來的 i n o r d e r . i n d e x ( ) inorder.index() inorder.index()來實現的,所以問題就在於構建雜湊表所使用的是一開始傳過來的 i n o r d e r inorder inorder,而自定義函式所使用的是切片,索引自然不同,為了讓這個構建的雜湊表能用上,還不能動一開始的 p r e o r d e r preorder preorder i n o r d e r inorder inorder。如何實現呢?

思考我們傳進去自定義函式的引數是切片,切片的核心就是原列表和索引,而原列表已經有了,所以我們可以將傳入切片換成傳入索引來實現,需要注意的是,將切片換為索引之後,求左子樹的遍歷列表長度不能直接用inorder_core_ndex變數的索引值,要手動計算。程式碼實現如下:

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        def mybuildTree(preorder_begin, preorder_end, inorder_begin, inorder_end):
            if preorder_begin > preorder_end:
                return None
            
            core = preorder[preorder_begin]
            inorder_core_index = index[core]
            left_size = inorder_core_index - inorder_begin # 左子樹的大小不再是根節點的索引值
            root = TreeNode(val=core)

            # 前序遍歷和中序遍歷的共同點是,遍歷結果的最後一個元素一定是當前樹的右下角的葉子元素
            root.left = mybuildTree(preorder_begin+1, left_size+preorder_begin, inorder_begin, inorder_core_index)
            root.right = mybuildTree(left_size+preorder_begin+1, preorder_end, inorder_core_index+1, inorder_end)
            return root

        length = len(preorder)
        # 構建雜湊對映表
        index = {val: ind for ind, val in enumerate(inorder)}
        return mybuildTree(0, length-1, 0, length-1)

時間複雜度: O ( n ) O(n) O(n),其中 n n n 是樹中的節點個數。

空間複雜度: O ( n ) O(n) O(n),除去返回的答案需要的 O ( n ) O(n) O(n)空間之外,我們還需要使用 O ( h ) O(h) O(h)(其中 h h h是樹的高度)的空間儲存棧。這裡 h < n h<n h<n,所以(在最壞情況下)總空間複雜度為 O ( n ) O(n) O(n)

將切片換成索引還有一個優點,記憶體消耗變小了,因為python中的切片其實是淺拷貝,對於非巢狀列表,淺拷貝之後的列表和原來的列表將不再是同一個列表,這就意味著將重新開闢一塊記憶體空間來儲存這段資料,將切片換成索引傳參之後就可以很好的解決這個問題。下圖中,上方的執行結果為改進之後的遞迴方法,下面的執行結果為改進前的遞迴方法,時間和記憶體消耗相差非常大。
在這裡插入圖片描述