Return any binary tree that matches the given preorder and postorder traversals.
Values in the traversals pre
and post
are distinct positive integers.
Example 1:
Input: pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
Output: [1,2,3,4,5,6,7]
Note:
1 <= pre.length == post.length <= 30
pre[]
andpost[]
are both permutations of1, 2, ..., pre.length
.- It is guaranteed an answer exists. If there exists multiple answers, you can return any of them.
這道題給了一棵樹的先序遍歷和后序遍歷的數組,讓我們根據這兩個數組來重建出原來的二叉樹。之前也做過二叉樹的先序遍歷 [Binary Tree Preorder Traversal](http://www.cnblogs.com/grandyang/p/4146981.html) 和 后序遍歷 [Binary Tree Postorder Traversal](http://www.cnblogs.com/grandyang/p/4251757.html),所以應該對其遍歷的順序並不陌生。其實二叉樹最常用的三種遍歷方式,先序,中序,和后序遍歷,只要知道其中的任意兩種遍歷得到的數組,就可以重建出原始的二叉樹,而且正好都在 LeetCode 中有出現,其他兩道分別是 [Construct Binary Tree from Inorder and Postorder Traversal](https://www.cnblogs.com/grandyang/p/4296193.html) 和 [Construct Binary Tree from Preorder and Inorder Traversal](https://www.cnblogs.com/grandyang/p/4296500.html)。如果做過之前兩道題,那么這道題就沒有什么難度了,若沒有的話,可能還是有些 tricky 的,雖然這僅僅只是一道 Medium 的題。
我們知道,先序遍歷的順序是 根->左->右,而后序遍歷的順序是 左->右->根,既然要建立樹,那么肯定要從根結點開始創建,然后再創建左右子結點,若你做過很多樹相關的題目的話,就會知道大多數都是用遞歸才做,那么創建的時候也是對左右子結點調用遞歸來創建。心中有這么個概念就好,可以繼續來找這個重復的 pattern。由於先序和后序各自的特點,根結點的位置是固定的,既是先序遍歷數組的第一個,又是后序遍歷數組的最后一個,而如果給我們的是中序遍歷的數組,那么根結點的位置就只能從另一個先序或者后序的數組中來找了,但中序也有中序的好處,其根結點正好分割了左右子樹,就不在這里細講了,還是回到本題吧。知道了根結點的位置后,我們需要分隔左右子樹的區間,先序和后序的各個區間表示如下:
preorder -> [root] [left subtree] [right subtree]
postorder -> [left subtree] [right substree] [root]
具體到題目中的例子就是:
preorder -> [1] [2,4,5] [3,6,7]
postorder -> [4,5,2] [6,7,3] [root]
先序和后序中各自的左子樹區間的長度肯定是相等的,但是其數字順序可能是不同的,但是我們仔細觀察的話,可以發現先序左子樹區間的第一個數字2,在后序左右子樹區間的最后一個位置,而且這個規律對右子樹區間同樣適用,這是為啥呢,這就要回到各自遍歷的順序了,先序遍歷的順序是 根->左->右,而后序遍歷的順序是 左->右->根,其實這個2就是左子樹的根結點,當然會一個在開頭,一個在末尾了。發現了這個規律,就可以根據其來定位左右子樹區間的位置范圍了。既然要拆分數組,那么就有兩種方式,一種是真的拆分成小的子數組,另一種是用雙指針來指向子區間的開頭和末尾。前一種方法無疑會有大量的數組拷貝,不是很高效,所以我們這里采用第二種方法來做。用 preL 和 preR 分別表示左子樹區間的開頭和結尾位置,postL 和 postR 表示右子樹區間的開頭和結尾位置,那么若 preL 大於 preR 或者 postL 大於 postR 的時候,說明已經不存在子樹區間,直接返回空指針。然后要先新建當前樹的根結點,就通過 pre[preL] 取到即可,接下來要找左子樹的根結點在 post 中的位置,最簡單的方法就是遍歷 post 中的區間 [postL, postR],找到其位置 idx,然后根據這個 idx,就可以算出左子樹區間長度為 len = (idx-postL)+1,那么 pre 數組中左子樹區間為 [preL+1, preL+len],右子樹區間為 [preL+1+len, preR],同理,post 數組中左子樹區間為 [postL, idx],右子樹區間為 [idx+1, postR-1]。知道了這些信息,就可以分別調用遞歸函數了,參見代碼如下:
解法一:
class Solution {
public:
TreeNode* constructFromPrePost(vector<int>& pre, vector<int>& post) {
return helper(pre, 0, (int)pre.size() - 1, post, 0, (int)post.size() - 1);
}
TreeNode* helper(vector<int>& pre, int preL, int preR, vector<int>& post, int postL, int postR) {
if (preL > preR || postL > postR) return nullptr;
TreeNode *node = new TreeNode(pre[preL]);
if (preL == preR) return node;
int idx = -1;
for (idx = postL; idx <= postR; ++idx) {
if (pre[preL + 1] == post[idx]) break;
}
node->left = helper(pre, preL + 1, preL + 1 + (idx - postL), post, postL, idx);
node->right = helper(pre, preL + 1 + (idx - postL) + 1, preR, post, idx + 1, postR - 1);
return node;
}
};
我們也可以使用 STL 內置的 find() 函數來查找左子樹的根結點在 post 中的位置,其余的地方都跟上面的解法相同,參見代碼如下:
解法二:
class Solution {
public:
TreeNode* constructFromPrePost(vector<int>& pre, vector<int>& post) {
return helper(pre, 0, (int)pre.size() - 1, post, 0, (int)post.size() - 1);
}
TreeNode* helper(vector<int>& pre, int preL, int preR, vector<int>& post, int postL, int postR) {
if (preL > preR || postL > postR) return nullptr;
TreeNode *node = new TreeNode(pre[preL]);
if (preL == preR) return node;
int idx = find(post.begin() + postL, post.begin() + postR + 1, pre[preL + 1]) - post.begin();
node->left = helper(pre, preL + 1, preL + 1 + (idx - postL), post, postL, idx);
node->right = helper(pre, preL + 1 + (idx - postL) + 1, preR, post, idx + 1, postR - 1);
return node;
}
};
為了進一步優化時間復雜度,我們可以事先用一個 HashMap,來建立 post 數組中每個元素和其坐標之間的映射,這樣在遞歸函數中,就不用進行查找了,直接在 HashMap 中將其位置取出來用即可,用空間換時間,也不失為一個好的方法,參見代碼如下:
解法三:
class Solution {
public:
TreeNode* constructFromPrePost(vector<int>& pre, vector<int>& post) {
unordered_map<int, int> m;
for (int i = 0; i < post.size(); ++i) m[post[i]] = i;
return helper(pre, 0, (int)pre.size() - 1, post, 0, (int)post.size() - 1, m);
}
TreeNode* helper(vector<int>& pre, int preL, int preR, vector<int>& post, int postL, int postR, unordered_map<int, int>& m) {
if (preL > preR || postL > postR) return nullptr;
TreeNode *node = new TreeNode(pre[preL]);
if (preL == preR) return node;
int idx = m[pre[preL + 1]], len = (idx - postL) + 1;
node->left = helper(pre, preL + 1, preL + len, post, postL, idx, m);
node->right = helper(pre, preL + 1 + len, preR, post, idx + 1, postR - 1, m);
return node;
}
};
論壇上 [lee215 大神](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/discuss/161268/C%2B%2BJavaPython-One-Pass-Real-O(N)) 提出了一種迭代的寫法,借助了棧來做,其實就用個數組就行,模擬棧的后進先出的特性。這種設計思路很巧妙,現根據 pre 數組進行先序創建二叉樹,當前我們的策略是,只要棧頂結點沒有左子結點,就把當前結點加到棧頂元素的左子結點上,否則加到右子結點上,並把加入的結點壓入棧。同時我們用兩個指針i和j分別指向當前在 pre 和 post 數組中的位置,若某個時刻,棧頂元素和 post[j] 相同了,說明當前子樹已經建立完成了,要將棧中當前的子樹全部出棧,直到 while 循環的條件不滿足。這樣最終建立下來,棧中就只剩下一個根結點了,返回即可,參見代碼如下:
解法四:
class Solution {
public:
TreeNode* constructFromPrePost(vector<int>& pre, vector<int>& post) {
vector<TreeNode*> st;
st.push_back(new TreeNode(pre[0]));
for (int i = 1, j = 0; i < pre.size(); ++i) {
TreeNode *node = new TreeNode(pre[i]);
while (st.back()->val == post[j]) {
st.pop_back();
++j;
}
if (!st.back()->left) st.back()->left = node;
else st.back()->right = node;
st.push_back(node);
}
return st[0];
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/889
類似題目:
Binary Tree Preorder Traversal
Binary Tree Postorder Traversal
Construct Binary Tree from Inorder and Postorder Traversal
Construct Binary Tree from Preorder and Inorder Traversal
參考資料:
https://leetcode.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)