题目
标题和出处
标题:从中序与后序遍历序列构造二叉树
难度
5 级
题目描述
要求
给定两个整数数组 inorder \texttt{inorder} inorder 和 postorder \texttt{postorder} postorder,其中 inorder \texttt{inorder} inorder 是二叉树的中序遍历, postorder \texttt{postorder} postorder 是同一个树的后序遍历,请构造并返回二叉树。
示例
示例 1:
输入: inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] \texttt{inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]} inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出: [3,9,20,null,null,15,7] \texttt{[3,9,20,null,null,15,7]} [3,9,20,null,null,15,7]
示例 2:
输入: inorder = [-1], postorder = [-1] \texttt{inorder = [-1], postorder = [-1]} inorder = [-1], postorder = [-1]
输出: [-1] \texttt{[-1]} [-1]
数据范围
- 1 ≤ inorder.length ≤ 3000 \texttt{1} \le \texttt{inorder.length} \le \texttt{3000} 1≤inorder.length≤3000
- postorder.length = inorder.length \texttt{postorder.length} = \texttt{inorder.length} postorder.length=inorder.length
- -3000 ≤ inorder[i], postorder[i] ≤ 3000 \texttt{-3000} \le \texttt{inorder[i], postorder[i]} \le \texttt{3000} -3000≤inorder[i], postorder[i]≤3000
- inorder \texttt{inorder} inorder 和 postorder \texttt{postorder} postorder 都由不同的值组成
- postorder \texttt{postorder} postorder 中的每个值均出现在 inorder \texttt{inorder} inorder 中
- inorder \texttt{inorder} inorder 保证为二叉树的中序遍历序列
- postorder \texttt{postorder} postorder 保证为二叉树的后序遍历序列
解法一
思路和算法
由于二叉树中的每个结点值各不相同,因此可以根据结点值唯一地确定结点。
二叉树的中序遍历的方法为:依次遍历左子树、根结点和右子树,对于左子树和右子树使用同样的方法遍历。
二叉树的后序遍历的方法为:依次遍历左子树、右子树和根结点,对于左子树和右子树使用同样的方法遍历。
后序遍历序列的最后一个元素值为根结点值,只要在中序遍历序列中定位到根结点值的下标,即可得到左子树中的结点数和右子树中的结点数。对于左子树和右子树,也可以在给定的中序遍历序列和后序遍历序列中分别得到对应的子序列,根据子序列构造相应的子树。当子树构造完毕时,原始二叉树即可构造完毕。
上述构造二叉树的过程是一个递归分治的过程。将二叉树分成根结点、左子树和右子树三部分,首先构造左子树和右子树,然后构造原始二叉树,构造左子树和右子树是原始问题的子问题。
分治的终止条件是子序列为空,此时构造的子树为空。当子序列不为空时,首先得到根结点值以及左子树和右子树对应的子序列,然后递归地构造左子树和右子树。
实现方面有两点需要注意。
-
在中序遍历序列中定位根结点值的下标时,简单的做法是遍历整个序列寻找根结点值,该做法的时间复杂度较高。可以使用哈希表存储每个结点值在中序遍历序列中的下标,即可在 O ( 1 ) O(1) O(1) 的时间内定位到任意结点值在中序遍历序列中的下标。
-
对于左子树和右子树的构造需要使用子序列,此处的子序列实质为下标连续的子数组。为了降低时间复杂度和空间复杂度,使用开始下标和子数组长度确定子数组,则不用新建数组和复制数组元素,而且可以复用哈希表存储的每个结点值在中序遍历序列中的下标信息。
代码
class Solution {
Map<Integer, Integer> inorderIndices = new HashMap<Integer, Integer>();
int[] inorder;
int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
this.inorder = inorder;
this.postorder = postorder;
int length = inorder.length;
for (int i = 0; i < length; i++) {
inorderIndices.put(inorder[i], i);
}
return buildTree(0, 0, length);
}
public TreeNode buildTree(int inorderStart, int postorderStart, int nodesCount) {
if (nodesCount == 0) {
return null;
}
int rootVal = postorder[postorderStart + nodesCount - 1];
TreeNode root = new TreeNode(rootVal);
int inorderRootIndex = inorderIndices.get(rootVal);
int leftNodesCount = inorderRootIndex - inorderStart;
int rightNodesCount = nodesCount - 1 - leftNodesCount;
root.left = buildTree(inorderStart, postorderStart, leftNodesCount);
root.right = buildTree(inorderRootIndex + 1, postorderStart + leftNodesCount, rightNodesCount);
return root;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 inorder \textit{inorder} inorder 和 postorder \textit{postorder} postorder 的长度,即二叉树的结点数。将中序遍历序列中的每个结点值与下标的对应关系存入哈希表需要 O ( n ) O(n) O(n) 的时间,构造二叉树也需要 O ( n ) O(n) O(n) 的时间。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 inorder \textit{inorder} inorder 和 postorder \textit{postorder} postorder 的长度,即二叉树的结点数。空间复杂度主要是递归调用的栈空间以及哈希表空间,因此空间复杂度是 O ( n ) O(n) O(n)。
解法二
思路和算法
使用迭代的方法构造二叉树,需要充分利用二叉树遍历的性质,考虑遍历序列中相邻结点的关系。
如果将中序遍历序列反序,则可以得到反序中序遍历序列,依次遍历右子树、根结点和左子树;如果将后序遍历序列反序,则可以得到反序后序遍历序列,依次遍历根结点、右子树和左子树。利用反序序列的性质,可以对中序遍历序列和后序遍历序列反向遍历并构造二叉树。注意构造二叉树的过程中只是对中序遍历序列和后序遍历序列反向遍历,并没有将中序遍历序列和后序遍历序列反序。
对于后序遍历序列中的两个相邻的值 x x x 和 y y y,其对应结点的关系一定是以下两种情况之一:
-
结点 x x x 是结点 y y y 的右子结点;
-
结点 y y y 没有右子结点,结点 x x x 是结点 y y y 的左子结点,或者结点 x x x 是结点 y y y 的某个祖先结点的左子结点。
对于第 1 种情况,在中序遍历序列中, y y y 在 x x x 前面且 y y y 和 x x x 相邻。对于第 2 种情况,在中序遍历序列中, x x x 在 y y y 前面。
判断后序遍历序列中的两个相邻的值属于哪一种情况,需要借助中序遍历序列。由于中序遍历访问左子树在访问根结点之前,因此可以比较后序遍历序列的下一个结点值(即下标加 1 1 1 的位置的结点值)和中序遍历序列的当前结点值,判断属于哪一种情况:
-
如果后序遍历序列的下一个结点值和中序遍历序列的当前结点值不同,则后序遍历序列的下一个结点有右子结点,属于第 1 种情况;
-
如果后序遍历序列的下一个结点值和中序遍历序列的当前结点值相同,则后序遍历序列的下一个结点没有右子结点,属于第 2 种情况。
注意在第 1 种情况下,中序遍历序列的结点值顺序恰好和后序遍历序列的结点值顺序相反,可以使用栈实现反转序列。
具体做法是,反向遍历后序遍历序列,对于每个值分别创建结点,将每个结点作为下一个结点的右子结点,并将每个结点入栈,直到后序遍历序列的下一个结点值等于中序遍历序列的当前结点值。然后反向遍历中序遍历序列并依次将栈内的结点出栈,直到栈顶结点值和中序遍历序列的当前结点值不同,此时后序遍历序列的当前值对应的结点为最后一个出栈的结点的左子结点,将当前结点入栈。然后对后序遍历序列和中序遍历序列的其余值继续执行上述操作,直到遍历结束时,二叉树构造完毕。
以下用一个例子说明构造二叉树的过程。已知二叉树的中序遍历序列是 [ 9 , 5 , 8 , 10 , 7 , 1 , 12 , 15 , 13 , 20 ] [9, 5, 8, 10, 7, 1, 12, 15, 13, 20] [9,5,8,10,7,1,12,15,13,20],后序遍历序列是 [ 5 , 9 , 8 , 7 , 10 , 12 , 13 , 20 , 15 , 1 ] [5, 9, 8, 7, 10, 12, 13, 20, 15, 1] [5,9,8,7,10,12,13,20,15,1],二叉树如图所示。
初始时,中序遍历序列的下标是 9 9 9。以下将中序遍历序列的下标处的值称为中序遍历序列的当前结点值。
-
将后序遍历序列的下标 9 9 9 的元素 1 1 1 作为根结点值创建根结点,并将根结点入栈。
-
当遍历到后序遍历序列的 15 15 15 时,下一个结点值和中序遍历序列的当前结点值不同,因此创建结点 15 15 15,作为结点 1 1 1 的右子结点,并将结点 15 15 15 入栈。
-
当遍历到后序遍历序列的 20 20 20 时,下一个结点值和中序遍历序列的当前结点值不同,因此创建结点 20 20 20,作为结点 15 15 15 的右子结点,并将结点 20 20 20 入栈。
-
当遍历到后序遍历序列的 13 13 13 时,下一个结点值是 20 20 20,和中序遍历序列的当前结点值相同,因此遍历中序遍历序列并将栈内的结点 20 20 20 出栈,此时中序遍历序列的下标移动到 8 8 8。创建结点 13 13 13,将结点 13 13 13 作为结点 20 20 20 的左子结点,并将结点 13 13 13 入栈。
-
当遍历到后序遍历序列的 12 12 12 时,下一个结点值是 13 13 13,和中序遍历序列的当前结点值相同,因此遍历中序遍历序列并将栈内的结点 13 13 13、 15 15 15 出栈,此时中序遍历序列的下标移动到 6 6 6。创建结点 12 12 12,将结点 12 12 12 作为结点 15 15 15 的左子结点,并将结点 12 12 12 入栈。
-
当遍历到后序遍历序列的 10 10 10 时,下一个结点值是 12 12 12,和中序遍历序列的当前结点值相同,因此遍历中序遍历序列并将栈内的结点 12 12 12、 1 1 1 出栈,此时中序遍历序列的下标移动到 4 4 4。创建结点 10 10 10,将结点 10 10 10 作为结点 1 1 1 的左子结点,并将结点 10 10 10 入栈。
-
当遍历到后序遍历序列的 7 7 7 时,下一个结点值和中序遍历序列的当前结点值不同,因此创建结点 7 7 7,作为结点 10 10 10 的右子结点,并将结点 7 7 7 入栈。
-
当遍历到后序遍历序列的 8 8 8 时,下一个结点值是 7 7 7,和中序遍历序列的当前结点值相同,因此遍历中序遍历序列并将栈内的结点 7 7 7、 10 10 10 出栈,此时中序遍历序列的下标移动到 2 2 2。创建结点 8 8 8,将结点 8 8 8 作为结点 10 10 10 的左子结点,并将结点 8 8 8 入栈。
-
当遍历到后序遍历序列的 9 9 9 时,下一个结点值是 8 8 8,和中序遍历序列的当前结点值相同,因此遍历中序遍历序列并将栈内的结点 8 8 8 出栈,此时中序遍历序列的下标移动到 1 1 1。创建结点 9 9 9,将结点 9 9 9 作为结点 8 8 8 的左子结点,并将结点 9 9 9 入栈。
-
当遍历到后序遍历序列的 5 5 5 时,下一个结点值和中序遍历序列的当前结点值不同,因此创建结点 5 5 5,作为结点 9 9 9 的右子结点,并将结点 5 5 5 入栈。
此时遍历结束,二叉树构造完毕。
代码
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
int length = postorder.length;
TreeNode root = new TreeNode(postorder[length - 1]);
Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
stack.push(root);
int inorderIndex = length - 1;
for (int i = length - 2; i >= 0; i--) {
TreeNode next = stack.peek();
TreeNode curr = new TreeNode(postorder[i]);
if (next.val != inorder[inorderIndex]) {
next.right = curr;
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
next = stack.pop();
inorderIndex--;
}
next.left = curr;
}
stack.push(curr);
}
return root;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 inorder \textit{inorder} inorder 和 postorder \textit{postorder} postorder 的长度,即二叉树的结点数。中序遍历序列和后序遍历序列各需要遍历一次,构造二叉树需要 O ( n ) O(n) O(n) 的时间。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 inorder \textit{inorder} inorder 和 postorder \textit{postorder} postorder 的长度,即二叉树的结点数。空间复杂度主要是栈空间,取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)。