中序+前序與中序+後序之重建二叉樹
- 2019 年 11 月 23 日
- 筆記
重建二叉樹
1.題目描述
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列 {1, 2, 4, 7, 3, 5, 6, 8} 和中序遍歷序列 {4, 7, 2, 1, 5, 3, 8, 6},則重建出如下圖所示的二叉樹並返回它的頭結點。
2.二叉樹四種遍歷方式
l例如一個二叉樹層次遍歷順序為[1,2,3,4,5,6,7],那麼:
前:[1, 2, 4, 5, 3, 6, 7]
中:[4, 2, 5, 1, 5, 3, 7]
後:[4, 5, 2, 6, 7, 3, 1]
層:[1, 2, 3, 4, 5, 6, 7]
- 前序遍歷:先訪問根結點,再訪問左子結點,最後訪問右子結點。
- 中序遍歷:先訪問左子結點,再訪問根結點,最後訪問右子結點。
- 後序遍歷:先訪問左子結點,再訪問右子結點,最後訪問根結點。
3.中序+前序
回到這個題目,我們知道中序+前序可以構建一顆二叉樹,而本題就是通過這個方式來構建,當然後序+中序也可以構建,但是前序+後序是不可以的。舉個例子:
D / E
前序遍歷:DE, 後序遍歷:ED
則下面樹的結構也滿足前序和後序遍歷的序列。
D E
即這種情況,不能唯一確定一棵樹。
當前這個題目(前序+中序)解決方案如下:
- 不使用原數組
- 使用原數組-採用左節點個數構建
- 使用原數組-採用右節點個數構建
(1)不使用原數組
思路很簡單,對比中序列與前序:
前序:[1, 2, 4, 5, 3, 6, 7] 中序:[4, 2, 5, 1, 5, 3, 7] i
首先構建根,其次遞歸構建自己的左右孩子,最後返回根就可以了。
每次在中序遍歷中找到根節點位置,然後劃分左右孩子。
TreeNode *reConstructBinaryTree(vector<int> pre, vector<int> vin) { if (pre.empty() || vin.empty()) return NULL; TreeNode *root = new TreeNode(pre[0]); vector<int> lpre, lvin, rpre, rvin; for (int i = 0; i < vin.size(); i++) { if (vin[i] == pre[0]) { lpre = vector<int>(pre.begin() + 1, pre.begin() + i + 1); lvin = vector<int>(vin.begin(), vin.begin() + i); rpre = vector<int>(pre.begin() + i + 1, pre.end()); rvin = vector<int>(vin.begin() + i + 1, vin.end()); root->left = reConstructBinaryTree(lpre, lvin); root->right = reConstructBinaryTree(rpre, rvin); } } return root; }
這種方法非常好理解,缺點是並不是在原來的數組上操作。
下面這種方法是直接在原來的數組上操作的,但是下標邊界直接看不是很明白,需要配合畫圖才能理解。
再看下面這個:
前序:[1, 2, 4, 5, 3, 6, 7] pStart pEnd 中序:[4, 2, 5, 1, 5, 3, 7] vStart i vEnd
中序遍歷得出如下結論:
- 左子結點長度 = i – vStart
- 右子結點長度 = vEnd – i
所以對於左子樹來說:
前序遍歷下標範圍 = [pStart+1,pStart+i-vStart]
中序遍歷下標範圍 = [vStart,i-1]
對於右子樹來說
前序遍歷下標範圍 = [pStart+i-vStart+1,pEnd]
中序遍歷下標範圍 = [i+1,vEnd]
於是第二種方法出來了。
(2)使用原數組-採用左節點個數構建
TreeNode *preForConstructBinaryTree(vector<int> pre, vector<int> vin) { return preAndVin(pre, 0, pre.size() - 1, vin, 0, vin.size() - 1); } // 前序+中序 TreeNode *preAndVin(const vector<int> &pre, int pStart, int pEnd, const vector<int> &vin,int vStart, int vEnd) { // 遞歸終止條件 if (pStart > pEnd || vStart > vEnd) return NULL; TreeNode *root = new TreeNode(pre[pStart]); // 重建根節點 // 重建左右子樹 for (int i = vStart; i <= vEnd; i++) { if (vin[i] == pre[pStart]) { root->left = preAndVin(pre, pStart + 1, pStart + i - vStart, vin, vStart, i - 1); root->right = preAndVin(pre, pStart + i - vStart + 1, pEnd, vin, i + 1, vEnd); } return root; }
(3)使用原數組-採用右節點個數構建
另外,我們還可以使用右子結點長度來構建。
所以對於左子樹來說:
前序遍歷下標範圍 = [pStart+1,pEnd-(vEnd-i)]
中序遍歷下標範圍 = [vStart,i-1]
對於右子樹來說
前序遍歷下標範圍 = [pEnd-(vEnd-i)+1,pEnd]
中序遍歷下標範圍 = [i+1,vEnd]
只需要將上述左右子樹構建改為:
root->left = preAndVin(pre, pStart + 1, pEnd - (vEnd - i), vin, vStart, i - 1); root->right = preAndVin(pre, pEnd - (vEnd - i) + 1, pEnd, vin, i + 1, vEnd);
4.中序+後序
既然這道題是前序+中序,那麼我們在琢磨一下中序+後序唄,同樣的道理,也是上面兩種。
(1)不使用原數組
TreeNode *reConstructBinaryTree1(vector<int> post, vector<int> vin) { if (post.empty() || vin.empty()) return NULL; TreeNode *root = new TreeNode(post[post.size() - 1]); vector<int> lpost, lvin, rpost, rvin; for (int i = 0; i < vin.size(); i++) { if (vin[i] == post[post.size() - 1]) { // 後序 左半邊 lpost = vector<int>(post.begin(), post.begin() + i); // 中序 左半邊 lvin = vector<int>(vin.begin(), vin.begin() + i); // 後序 右半邊 rpost = vector<int>(post.begin() + i, post.end() - 1); // 前序 右半邊 rvin = vector<int>(vin.begin() + i + 1, vin.end()); root->left = reConstructBinaryTree1(lpost, lvin); root->right = reConstructBinaryTree1(rpost, rvin); } } return root; }
(2)使用原數組-採用左節點個數構建
後:[4, 5, 2, 6, 7, 3, 1] pStart pEnd 中:[4, 2, 5, 1, 5, 3, 7] vStar i vEnd
- 左子結點長度 = i – vStart
- 右子結點長度 = vEnd – i
所以對於左子樹來說:
後序遍歷下標範圍 = [pStart,pStart+i-vStart-1]
中序遍歷下標範圍 = [vStart,i-1]
對於右子樹來說
後序遍歷下標範圍 = [pStart+i-vStart,pEnd-1]
中序遍歷下標範圍 = [i+1,vEnd]
// 中序+後序 TreeNode *postAndVin(const vector<int> &post, int pStart, int pEnd, const vector<int> &vin,int vStart, int vEnd) { // 遞歸終止條件 if (pStart > pEnd || vStart > vEnd) return NULL; int rootVal = post[pEnd]; TreeNode *root = new TreeNode(rootVal); // 重建根節點 // 重建左右子樹 for (int i = vStart; i <= vEnd; i++) { if (vin[i] == rootVal) { root->left = postAndVin(post, pStart, pStart + i - vStart - 1, vin, vStart, i - 1); root->right = postAndVin(post, pStart + i - vStart, pEnd - 1, vin, i + 1, vEnd); } } return root; } };
(3)使用原數組-採用右節點個數構建
所以對於左子樹來說:
前序遍歷下標範圍 = [pStart,pEnd – (vEnd – i) – 1]
中序遍歷下標範圍 = [vStart,i-1]
對於右子樹來說
後序遍歷下標範圍 = [pEnd-(vEnd-i),pEnd-1]
中序遍歷下標範圍 = [i+1,vEnd]
所以,上述程式碼改為:
// method 2 root->left = postAndVin(post, pStart, pEnd - (vEnd - i) - 1, vin, vStart, i - 1); root->right = postAndVin(post, pEnd - (vEnd - i), pEnd - 1, vin, i + 1, vEnd);