從前序及中序與后序遍歷序列構造二叉樹登場!
看完本文,可以一起解決如下兩道題目
- 從中序與后序遍歷序列構造二叉樹
- 從前序與中序遍歷序列構造二叉樹
從中序與后序遍歷序列構造二叉樹題
目地址:https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/
根據一棵樹的中序遍歷與后序遍歷構造二叉樹。
注意: 你可以假設樹中沒有重復的元素。
例如,給出
中序遍歷 inorder = [9,3,15,20,7] 后序遍歷 postorder = [9,15,7,20,3] 返回如下的二叉樹:

思路
首先回憶一下如何根據兩個順序構造一個唯一的二叉樹,相信理論知識大家應該都清楚,就是以 后序數組的最后一個元素為切割點,先切中序數組,根據中序數組,反過來在切后序數組。一層一層切下去,每次后序數組最后一個元素就是節(jié)點元素。
如果讓我們肉眼看兩個序列,畫一顆二叉樹的話,應該分分鐘都可以畫出來。
流程如圖:

從中序與后序遍歷序列構造二叉樹
那么代碼應該怎么寫呢?
說到一層一層切割,就應該想到了遞歸。
來看一下一共分幾步:
- 第一步:如果數組大小為零的話,說明是空節(jié)點了。
- 第二步:如果不為空,那么取后序數組最后一個元素作為節(jié)點元素。
- 第三步:找到后序數組最后一個元素在中序數組的位置,作為切割點
- 第四步:切割中序數組,切成中序左數組和中序右數組 (順序別搞反了,一定是先切中序數組)
- 第五步:切割后序數組,切成后序左數組和后序右數組
- 第六步:遞歸處理左區(qū)間和右區(qū)間
不難寫出如下代碼:(先把框架寫出來)
- TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
- // 第一步
- if (postorder.size() == 0) return NULL;
- // 第二步:后序遍歷數組最后一個元素,就是當前的中間節(jié)點
- int rootValue = postorder[postorder.size() - 1];
- TreeNode* root = new TreeNode(rootValue);
- // 葉子節(jié)點
- if (postorder.size() == 1) return root;
- // 第三步:找切割點
- int delimiterIndex;
- for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
- if (inorder[delimiterIndex] == rootValue) break;
- }
- // 第四步:切割中序數組,得到 中序左數組和中序右數組
- // 第五步:切割后序數組,得到 后序左數組和后序右數組
- // 第六步
- root->left = traversal(中序左數組, 后序左數組);
- root->right = traversal(中序右數組, 后序右數組);
- return root;
- }
難點大家應該發(fā)現了,就是如何切割,以及邊界值找不好很容易亂套。
此時應該注意確定切割的標準,是左閉右開,還有左開又閉,還是左閉又閉,這個就是不變量,要在遞歸中保持這個不變量。
在切割的過程中會產生四個區(qū)間,把握不好不變量的話,一會左閉右開,一會左閉又閉,必然亂套!
我在704.二分查找和59.螺旋矩陣II中都強調過循環(huán)不變量的重要性,在二分查找以及螺旋矩陣的求解中,堅持循環(huán)不變量非常重要,本題也是。
首先要切割中序數組,為什么先切割中序數組呢?
切割點在后序數組的最后一個元素,就是用這個元素來切割中序數組的,所以必要先切割中序數組。
中序數組相對比較好切,找到切割點(后序數組的最后一個元素)在中序數組的位置,然后切割,如下代碼中我堅持左閉右開的原則:
- // 找到中序遍歷的切割點
- int delimiterIndex;
- for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
- if (inorder[delimiterIndex] == rootValue) break;
- }
- // 左閉右開區(qū)間:[0, delimiterIndex)
- vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
- // [delimiterIndex + 1, end)
- vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );
接下來就要切割后序數組了。
首先后序數組的最后一個元素指定不能要了,這是切割點 也是 當前二叉樹中間節(jié)點的元素,已經用了。
后序數組的切割點怎么找?
后序數組沒有明確的切割元素來進行左右切割,不像中序數組有明確的切割點,切割點左右分開就可以了。
此時有一個很重的點,就是中序數組大小一定是和后序數組的大小相同的(這是必然)。
中序數組我們都切成了左中序數組和右中序數組了,那么后序數組就可以按照左中序數組的大小來切割,切成左后序數組和右后序數組。
代碼如下:
- // postorder 舍棄末尾元素,因為這個元素就是中間節(jié)點,已經用過了
- postorder.resize(postorder.size() - 1);
- // 左閉右開,注意這里使用了左中序數組大小作為切割點:[0, leftInorder.size)
- vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
- // [leftInorder.size(), end)
- vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
此時,中序數組切成了左中序數組和右中序數組,后序數組切割成左后序數組和右后序數組。
接下來可以遞歸了,代碼如下:
- root->left = traversal(leftInorder, leftPostorder);
- root->right = traversal(rightInorder, rightPostorder);
完整代碼如下:
C++完整代碼
- class Solution {
- private:
- TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
- if (postorder.size() == 0) return NULL;
- // 后序遍歷數組最后一個元素,就是當前的中間節(jié)點
- int rootValue = postorder[postorder.size() - 1];
- TreeNode* root = new TreeNode(rootValue);
- // 葉子節(jié)點
- if (postorder.size() == 1) return root;
- // 找到中序遍歷的切割點
- int delimiterIndex;
- for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
- if (inorder[delimiterIndex] == rootValue) break;
- }
- // 切割中序數組
- // 左閉右開區(qū)間:[0, delimiterIndex)
- vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
- // [delimiterIndex + 1, end)
- vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );
- // postorder 舍棄末尾元素
- postorder.resize(postorder.size() - 1);
- // 切割后序數組
- // 依然左閉右開,注意這里使用了左中序數組大小作為切割點
- // [0, leftInorder.size)
- vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
- // [leftInorder.size(), end)
- vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
- root->left = traversal(leftInorder, leftPostorder);
- root->right = traversal(rightInorder, rightPostorder);
- return root;
- }
- public:
- TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
- if (inorder.size() == 0 || postorder.size() == 0) return NULL;
- return traversal(inorder, postorder);
- }
- };
相信大家自己就算是思路清晰, 代碼寫出來一定是各種問題,所以一定要加日志來調試,看看是不是按照自己思路來切割的,不要大腦模擬,那樣越想越糊涂。
下面給出用下表索引寫出的代碼版本:(思路是一樣的,只不過不用重復定義vector了,每次用下表索引來分割)
C++優(yōu)化版本
那么這個版本寫出來依然要打日志進行調試,打日志的版本如下:(該版本不要在leetcode上提交,容易超時)
- class Solution {
- private:
- TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd) {
- if (postorderBegin == postorderEnd) return NULL;
- int rootValue = postorder[postorderEnd - 1];
- TreeNode* root = new TreeNode(rootValue);
- if (postorderEnd - postorderBegin == 1) return root;
- int delimiterIndex;
- for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
- if (inorder[delimiterIndex] == rootValue) break;
- }
- // 切割中序數組
- // 左中序區(qū)間,左閉右開[leftInorderBegin, leftInorderEnd)
- int leftInorderBegin = inorderBegin;
- int leftInorderEnd = delimiterIndex;
- // 右中序區(qū)間,左閉右開[rightInorderBegin, rightInorderEnd)
- int rightInorderBegin = delimiterIndex + 1;
- int rightInorderEnd = inorderEnd;
- // 切割后序數組
- // 左后序區(qū)間,左閉右開[leftPostorderBegin, leftPostorderEnd)
- int leftPostorderBegin = postorderBegin;
- int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 終止位置是 需要加上 中序區(qū)間的大小size
- // 右后序區(qū)間,左閉右開[rightPostorderBegin, rightPostorderEnd)
- int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin);
- int rightPostorderEnd = postorderEnd - 1; // 排除最后一個元素,已經作為節(jié)點了
- cout << "----------" << endl;
- cout << "leftInorder :";
- for (int i = leftInorderBegin; i < leftInorderEnd; i++) {
- cout << inorder[i] << " ";
- }
- cout << endl;
- cout << "rightInorder :";
- for (int i = rightInorderBegin; i < rightInorderEnd; i++) {
- cout << inorder[i] << " ";
- }
- cout << endl;
- cout << "leftpostorder :";
- for (int i = leftPostorderBegin; i < leftPostorderEnd; i++) {
- cout << postorder[i] << " ";
- }
- cout << endl;
- cout << "rightpostorder :";
- for (int i = rightPostorderBegin; i < rightPostorderEnd; i++) {
- cout << postorder[i] << " ";
- }
- cout << endl;
- root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd);
- root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd);
- return root;
- }
- public:
- TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
- if (inorder.size() == 0 || postorder.size() == 0) return NULL;
- return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size());
- }
- };
從前序與中序遍歷序列構造二叉樹
題目地址:https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
根據一棵樹的前序遍歷與中序遍歷構造二叉樹。
注意: 你可以假設樹中沒有重復的元素。
例如,給出
前序遍歷 preorder = [3,9,20,15,7] 中序遍歷 inorder = [9,3,15,20,7] 返回如下的二叉樹:

從前序與中序遍歷序列構造二叉樹
思路
本題和106是一樣的道理。
我就直接給出代碼了。
- class Solution {
- private:
- TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& preorder, int preorderBegin, int preorderEnd) {
- if (preorderBegin == preorderEnd) return NULL;
- int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0
- TreeNode* root = new TreeNode(rootValue);
- if (preorderEnd - preorderBegin == 1) return root;
- int delimiterIndex;
- for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
- if (inorder[delimiterIndex] == rootValue) break;
- }
- // 切割中序數組
- // 中序左區(qū)間,左閉右開[leftInorderBegin, leftInorderEnd)
- int leftInorderBegin = inorderBegin;
- int leftInorderEnd = delimiterIndex;
- // 中序右區(qū)間,左閉右開[rightInorderBegin, rightInorderEnd)
- int rightInorderBegin = delimiterIndex + 1;
- int rightInorderEnd = inorderEnd;
- // 切割前序數組
- // 前序左區(qū)間,左閉右開[leftPreorderBegin, leftPreorderEnd)
- int leftPreorderBegin = preorderBegin + 1;
- int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 終止位置是起始位置加上中序左區(qū)間的大小size
- // 前序右區(qū)間, 左閉右開[rightPreorderBegin, rightPreorderEnd)
- int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
- int rightPreorderEnd = preorderEnd;
- root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd);
- root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);
- return root;
- }
- public:
- TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
- if (inorder.size() == 0 || preorder.size() == 0) return NULL;
- // 參數堅持左閉右開的原則
- return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size());
- }
- };
思考題
前序和中序可以唯一確定一顆二叉樹。
后序和中序可以唯一確定一顆二叉樹。
那么前序和后序可不可以唯一確定一顆二叉樹呢?
前序和后序不能唯一確定一顆二叉樹!,因為沒有中序遍歷無法確定左右部分,也就是無法分割。
舉一個例子:

從中序與后序遍歷序列構造二叉樹2
tree1 的前序遍歷是[1 2 3], 后序遍歷是[3 2 1]。
tree2 的前序遍歷是[1 2 3], 后序遍歷是[3 2 1]。
那么tree1 和 tree2 的前序和后序完全相同,這是一棵樹么,很明顯是兩棵樹!
所以前序和后序不能唯一確定一顆二叉樹!
總結
之前我們講的二叉樹題目都是各種遍歷二叉樹,這次開始構造二叉樹了,思路其實比較簡單,但是真正代碼實現出來并不容易。
所以要避免眼高手低,踏實的把代碼寫出來。
我同時給出了添加日志的代碼版本,因為這種題目是不太容易寫出來調一調就能過的,所以一定要把流程日志打出來,看看符不符合自己的思路。
大家遇到這種題目的時候,也要學會打日志來調試(如何打日志有時候也是個技術活),不要腦動模擬,腦動模擬很容易越想越亂。
最后我還給出了為什么前序和中序可以唯一確定一顆二叉樹,后序和中序可以唯一確定一顆二叉樹,而前序和后序卻不行。
認真研究完本篇,相信大家對二叉樹的構造會清晰很多。
其他語言版本
Java
從中序與后序遍歷序列構造二叉樹
- class Solution {
- public TreeNode buildTree(int[] inorder, int[] postorder) {
- return buildTree1(inorder, 0, inorder.length, postorder, 0, postorder.length);
- }
- public TreeNode buildTree1(int[] inorder, int inLeft, int inRight,
- int[] postorder, int postLeft, int postRight) {
- // 沒有元素了
- if (inRight - inLeft < 1) {
- return null;
- }
- // 只有一個元素了
- if (inRight - inLeft == 1) {
- return new TreeNode(inorder[inLeft]);
- }
- // 后序數組postorder里最后一個即為根結點
- int rootVal = postorder[postRight - 1];
- TreeNode root = new TreeNode(rootVal);
- int rootIndex = 0;
- // 根據根結點的值找到該值在中序數組inorder里的位置
- for (int i = inLeft; i < inRight; i++) {
- if (inorder[i] == rootVal) {
- rootIndex = i;
- }
- }
- // 根據rootIndex劃分左右子樹
- root.left = buildTree1(inorder, inLeft, rootIndex,
- postorder, postLeft, postLeft + (rootIndex - inLeft));
- root.right = buildTree1(inorder, rootIndex + 1, inRight,
- postorder, postLeft + (rootIndex - inLeft), postRight - 1);
- return root;
- }
- }
從前序與中序遍歷序列構造二叉樹
- class Solution {
- public TreeNode buildTree(int[] preorder, int[] inorder) {
- return helper(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
- }
- public TreeNode helper(int[] preorder, int preLeft, int preRight,
- int[] inorder, int inLeft, int inRight) {
- // 遞歸終止條件
- if (inLeft > inRight || preLeft > preRight) return null;
- // val 為前序遍歷第一個的值,也即是根節(jié)點的值
- // idx 為根據根節(jié)點的值來找中序遍歷的下標
- int idx = inLeft, val = preorder[preLeft];
- TreeNode root = new TreeNode(val);
- for (int i = inLeft; i <= inRight; i++) {
- if (inorder[i] == val) {
- idx = i;
- break;
- }
- }
- // 根據 idx 來遞歸找左右子樹
- root.left = helper(preorder, preLeft + 1, preLeft + (idx - inLeft),
- inorder, inLeft, idx - 1);
- root.right = helper(preorder, preLeft + (idx - inLeft) + 1, preRight,
- inorder, idx + 1, inRight);
- return root;
- }
- }
Python
從前序與中序遍歷序列構造二叉樹
- class Solution:
- def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
- # 第一步: 特殊情況討論: 樹為空. 或者說是遞歸終止條件
- if not preorder:
- return None
- # 第二步: 前序遍歷的第一個就是當前的中間節(jié)點.
- root_val = preorder[0]
- root = TreeNode(root_val)
- # 第三步: 找切割點.
- separator_idx = inorder.index(root_val)
- # 第四步: 切割inorder數組. 得到inorder數組的左,右半邊.
- inorder_left = inorder[:separator_idx]
- inorder_right = inorder[separator_idx + 1:]
- # 第五步: 切割preorder數組. 得到preorder數組的左,右半邊.
- # ⭐️ 重點1: 中序數組大小一定跟前序數組大小是相同的.
- preorder_left = preorder[1:1 + len(inorder_left)]
- preorder_right = preorder[1 + len(inorder_left):]
- # 第六步: 遞歸
- root.left = self.buildTree(preorder_left, inorder_left)
- root.right = self.buildTree(preorder_right, inorder_right)
- return root
從中序與后序遍歷序列構造二叉樹
- class Solution:
- def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
- # 第一步: 特殊情況討論: 樹為空. (遞歸終止條件)
- if not postorder:
- return None
- # 第二步: 后序遍歷的最后一個就是當前的中間節(jié)點.
- root_val = postorder[-1]
- root = TreeNode(root_val)
- # 第三步: 找切割點.
- separator_idx = inorder.index(root_val)
- # 第四步: 切割inorder數組. 得到inorder數組的左,右半邊.
- inorder_left = inorder[:separator_idx]
- inorder_right = inorder[separator_idx + 1:]
- # 第五步: 切割postorder數組. 得到postorder數組的左,右半邊.
- # ⭐️ 重點1: 中序數組大小一定跟后序數組大小是相同的.
- postorder_left = postorder[:len(inorder_left)]
- postorder_right = postorder[len(inorder_left): len(postorder) - 1]
- # 第六步: 遞歸
- root.left = self.buildTree(inorder_left, postorder_left)
- root.right = self.buildTree(inorder_right, postorder_right)
- return root