http://blog.bittiger.io/post196/?utm_source=mailchimp&utm_medium=dailyemail&utm_campaign=svtalk&utm_content=pm-article-2-20180211
大家好,今天我們的問題是給定一個二叉樹的中序遍歷數據序列和一個前序遍歷數據序列,寫一個重現這棵二叉樹。假設每個節點都是唯一的值。例如給定序列如下:
中序遍歷(in-order):F B A E H C D I G
前序遍歷(pre-order):H B F E A C D G I
需要重現的二叉樹如下圖:
重溫基礎知識
這個問題乍一看是有一點困難,讓我們先來重溫一下關於中序遍歷()和前序遍歷的基礎知識。
中序遍歷
對於中序遍歷的規則是首先找出所有左子樹的節點,然後是根節點,最後是右子樹的節點。以上一張二叉樹圖爲例:
第一步,遍歷根節點H的左子樹,將其標記爲H’L,然後H的右子樹標記爲H’R,則整棵樹可以視作(H’L) H (H’R)。
第二步,單看H的左子樹,發現這個左子樹的根節點是B,則將這顆左子樹視作(B’L) B(B’R),則整棵樹可以視作(B’L) B (B’R) H (H’R)。
第三步,B的左子樹只有一個F,所以F就被記錄下來,添加到序列中,則整棵樹可以視作F B (B’R) H (H’R)。
第四步,B的右子樹,E是根節點,左子樹是A,沒有右子樹,所以E和A也被添加到序列中,則整棵樹可以視作F B A E H (H’R)。
以此類推,得到中序遍歷結果爲FB A E H C D I G。
前序遍歷
前序遍歷與中序遍歷類似,但它的規則是首先找到根節點,然後是左子樹,然後是右子樹。以上一張二叉樹圖爲例:
第一步,遍歷根節點H的左子樹,將其標記爲H’L,然後H的右子樹標記爲H’R,則整棵樹可以視作H (H’L) (H’R)。
第二步,單看H的左子樹,發現這個左子樹的根節點是B,則將這顆左子樹視作B (B’L)(B’R),則整棵樹可以視作H B (B’L) (B’R) (H’R)。
第三步,B的左子樹只有一個F,所以F就被記錄下來,添加到序列中,則整棵樹可以視作H B F (B’R) (H’R)。
第四步,B的右子樹,E是根節點,左子樹是A,沒有右子樹,所以E和A也被添加到序列中,則整棵樹可以視作H B F E A (H’R)。
以此類推,得到中序遍歷結果爲HB F E A C D G I。
問題解決
有一個簡單粗暴的方法來解決文章開始提出的問題,就基於中序遍歷數據序列,我們可以窮舉每個二叉樹。然後再用前序遍歷進行驗證。因爲有太多種可能,而且每次驗證的時間複雜度都是O(n),所以總的時間是非常巨大的。
我們需要一些改進去提升算法的性能。我們都知道前序遍歷一定會把整個樹的根節點放到最前面,通過前序遍歷數據序列的第一個元素,就能知道整棵樹的根節點,這樣我們就能在中序遍歷數據序列找到適當的索引去區分左子樹和右子樹。在這個例子中,前序的第一個元素是H,所以根節點就是H,F B A E 就是左子樹(標藍),CD I G 就是右子樹(標綠),如下所示:
中序遍歷(in-order):F B A E H C D I G
前序遍歷(pre-order):H B F E A C D G I
顯而易見,藍色的左子樹和綠色的右子樹又可以藉助中序遍歷和前序遍歷的特徵通過不斷重複迭代(repeatrecursively),來重現整個樹,如下圖:
查看前序遍歷的第一個元素,我們發現左子樹的根節點是B,右子樹的根節點是C,由此又可以得到新的左右子樹,如下圖所示
以此類推,最終得到如下圖的二叉樹:
明白了上面的過程,我們就會多一個問題,當我們知道了根節點,如何在中序遍歷序列數據中找到節點對應索引。我們可以繼續一個線性查詢(linearsearch),即當我們一找到根節點就跳出,雖然每次查詢是線性的,但是當我們遞歸調用的時候,我們需要爲每個節點尋找對應索引,那麼時間複雜度就變成了O(n²)。爲此我們可以通過“空間換時間”的方法,優化性能。那麼如何做呢?
算法提升
我們可以建立一個hash表去記錄每個元素的索引,key用來存放節點,value用來存放索引,所以當給出一個節點,我們立馬去查詢hash表就可以直接得到索引,不需要再去通過查詢數組來得到對應的索引。這個方法的時間複雜度是線性的,因爲每個元素,我們只會去訪問一次,所以時間複雜度是O(n+h)≈O(n)。
首先,有一個方法去構建一個hash表,並記錄每個元素的索引,然後調用遞歸的helper方法得到整棵樹,構建hash和調用helper方法的代碼如下:
helper方法是遞歸方法,它的入參有前序遍歷的列表,前序遍歷的起始、結束標號,中序遍歷的起始、結束標號以及兩種遍歷對應標號的hash映射,helper方法代碼實現如下:
注意點:
1.基本情況(紅框),由於是遞歸調用,一定要定義一個最簡單的邊界條件(boundarycondition),在我們這個遞歸調用時,如果遇到邊界是非法的,就會返回null。
2.從前面的介紹中我們可以知道,在前序遍歷和中序遍歷中,葉子節點一定只有一個入口(entry),所以需要將葉子節點的左子樹和右子樹設置爲null。
3.遞歸調用返回左子樹(藍框),前序遍歷的結束標誌(比如前例中H B F E A C D G I中前序遍歷的開始標誌爲1(B),結束標誌爲0+1+4=5(C),左閉右開的區間)是基於左子樹的長度的得到的。
4.遞歸調用返回右子樹(綠框),原理類似藍框中邏輯。
思考題
目前是已知前序遍歷和中序遍歷求二叉樹,那如果知道前序遍歷和後序遍歷或者知道中序遍歷和後序遍歷求二叉樹應該如何處理呢?歡迎你留言在下方。