原文链接: https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal
英文原文
Given two integer arrays preorder and inorder where preorder is the preorder traversal of a binary tree and inorder is the inorder traversal of the same tree, construct and return the binary tree.
Example 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] Output: [3,9,20,null,null,15,7]
Example 2:
Input: preorder = [-1], inorder = [-1] Output: [-1]
Constraints:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorderandinorderconsist of unique values.- Each value of
inorderalso appears inpreorder. preorderis guaranteed to be the preorder traversal of the tree.inorderis guaranteed to be the inorder traversal of the tree.
中文题目
给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
示例 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] Output: [3,9,20,null,null,15,7]
示例 2:
Input: preorder = [-1], inorder = [-1] Output: [-1]
提示:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder和inorder均无重复元素inorder均出现在preorderpreorder保证为二叉树的前序遍历序列inorder保证为二叉树的中序遍历序列
通过代码
高赞题解
📺视频题解
📖文字题解
前言
二叉树前序遍历的顺序为:
先遍历根节点;
随后递归地遍历左子树;
最后递归地遍历右子树。
二叉树中序遍历的顺序为:
先递归地遍历左子树;
随后遍历根节点;
最后递归地遍历右子树。
在「递归」地遍历某个子树的过程中,我们也是将这颗子树看成一颗全新的树,按照上述的顺序进行遍历。挖掘「前序遍历」和「中序遍历」的性质,我们就可以得出本题的做法。
方法一:递归
思路
对于任意一颗树而言,前序遍历的形式总是
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。
这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
细节
在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要 $O(1)$ 的时间对根节点进行定位了。
下面的代码给出了详细的注释。
[sol1-C++]class Solution { private: unordered_map<int, int> index; public: TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) { if (preorder_left > preorder_right) { return nullptr; } // 前序遍历中的第一个节点就是根节点 int preorder_root = preorder_left; // 在中序遍历中定位根节点 int inorder_root = index[preorder[preorder_root]]; // 先把根节点建立出来 TreeNode* root = new TreeNode(preorder[preorder_root]); // 得到左子树中的节点数目 int size_left_subtree = inorder_root - inorder_left; // 递归地构造左子树,并连接到根节点 // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1); // 递归地构造右子树,并连接到根节点 // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right); return root; } TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { int n = preorder.size(); // 构造哈希映射,帮助我们快速定位根节点 for (int i = 0; i < n; ++i) { index[inorder[i]] = i; } return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1); } };
[sol1-Java]class Solution { private Map<Integer, Integer> indexMap; public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) { if (preorder_left > preorder_right) { return null; } // 前序遍历中的第一个节点就是根节点 int preorder_root = preorder_left; // 在中序遍历中定位根节点 int inorder_root = indexMap.get(preorder[preorder_root]); // 先把根节点建立出来 TreeNode root = new TreeNode(preorder[preorder_root]); // 得到左子树中的节点数目 int size_left_subtree = inorder_root - inorder_left; // 递归地构造左子树,并连接到根节点 // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1); // 递归地构造右子树,并连接到根节点 // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right); return root; } public TreeNode buildTree(int[] preorder, int[] inorder) { int n = preorder.length; // 构造哈希映射,帮助我们快速定位根节点 indexMap = new HashMap<Integer, Integer>(); for (int i = 0; i < n; i++) { indexMap.put(inorder[i], i); } return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1); } }
[sol1-Python3]class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int): if preorder_left > preorder_right: return None # 前序遍历中的第一个节点就是根节点 preorder_root = preorder_left # 在中序遍历中定位根节点 inorder_root = index[preorder[preorder_root]] # 先把根节点建立出来 root = TreeNode(preorder[preorder_root]) # 得到左子树中的节点数目 size_left_subtree = inorder_root - inorder_left # 递归地构造左子树,并连接到根节点 # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1) # 递归地构造右子树,并连接到根节点 # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right) return root n = len(preorder) # 构造哈希映射,帮助我们快速定位根节点 index = {element: i for i, element in enumerate(inorder)} return myBuildTree(0, n - 1, 0, n - 1)
[sol1-Golang]func buildTree(preorder []int, inorder []int) *TreeNode { if len(preorder) == 0 { return nil } root := &TreeNode{preorder[0], nil, nil} i := 0 for ; i < len(inorder); i++ { if inorder[i] == preorder[0] { break } } root.Left = buildTree(preorder[1:len(inorder[:i])+1], inorder[:i]) root.Right = buildTree(preorder[len(inorder[:i])+1:], inorder[i+1:]) return root }
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 是树中的节点个数。
空间复杂度:$O(n)$,除去返回的答案需要的 $O(n)$ 空间之外,我们还需要使用 $O(n)$ 的空间存储哈希映射,以及 $O(h)$(其中 $h$ 是树的高度)的空间表示递归时栈空间。这里 $h < n$,所以总空间复杂度为 $O(n)$。
方法二:迭代
思路
迭代法是一种非常巧妙的实现方法。
对于前序遍历中的任意两个连续节点 $u$ 和 $v$,根据前序遍历的流程,我们可以知道 $u$ 和 $v$ 只有两种可能的关系:
$v$ 是 $u$ 的左儿子。这是因为在遍历到 $u$ 之后,下一个遍历的节点就是 $u$ 的左儿子,即 $v$;
$u$ 没有左儿子,并且 $v$ 是 $u$ 的某个祖先节点(或者 $u$ 本身)的右儿子。如果 $u$ 没有左儿子,那么下一个遍历的节点就是 $u$ 的右儿子。如果 $u$ 没有右儿子,我们就会向上回溯,直到遇到第一个有右儿子(且 $u$ 不在它的右儿子的子树中)的节点 $u_a$,那么 $v$ 就是 $u_a$ 的右儿子。
第二种关系看上去有些复杂。我们举一个例子来说明其正确性,并在例子中给出我们的迭代算法。
例子
我们以树
3
/ \
9 20
/ / \
8 15 7
/ \
5 10
/
4
为例,它的前序遍历和中序遍历分别为
preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]
我们用一个栈 stack 来维护「当前节点的所有还没有考虑过右儿子的祖先节点」,栈顶就是当前节点。也就是说,只有在栈中的节点才可能连接一个新的右儿子。同时,我们用一个指针 index 指向中序遍历的某个位置,初始值为 0。index 对应的节点是「当前节点不断往左走达到的最终节点」,这也是符合中序遍历的,它的作用在下面的过程中会有所体现。
首先我们将根节点 3 入栈,再初始化 index 所指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。
我们遍历
9。9一定是栈顶节点3的左儿子。我们使用反证法,假设9是3的右儿子,那么3没有左儿子,index应该恰好指向3,但实际上为4,因此产生了矛盾。所以我们将9作为3的左儿子,并将9入栈。stack = [3, 9]index -> inorder[0] = 4
我们遍历
8,5和4。同理可得它们都是上一个节点(栈顶节点)的左儿子,所以它们会依次入栈。stack = [3, 9, 8, 5, 4]index -> inorder[0] = 4
我们遍历
10,这时情况就不一样了。我们发现index恰好指向当前的栈顶节点4,也就是说4没有左儿子,那么10必须为栈中某个节点的右儿子。那么如何找到这个节点呢?栈中的节点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个节点的右儿子都还没有被遍历过,那么这些节点的顺序和它们在中序遍历中出现的顺序一定是相反的。这是因为栈中的任意两个相邻的节点,前者都是后者的某个祖先。并且我们知道,栈中的任意一个节点的右儿子还没有被遍历过,说明后者一定是前者左儿子的子树中的节点,那么后者就先于前者出现在中序遍历中。
因此我们可以把
index不断向右移动,并与栈顶节点进行比较。如果index对应的元素恰好等于栈顶节点,那么说明我们在中序遍历中找到了栈顶节点,所以将index增加1并弹出栈顶节点,直到index对应的元素不等于栈顶节点。按照这样的过程,我们弹出的最后一个节点x就是10的双亲节点,这是因为10出现在了x与x在栈中的下一个节点的中序遍历之间,因此10就是x的右儿子。回到我们的例子,我们会依次从栈顶弹出
4,5和8,并且将index向右移动了三次。我们将10作为最后弹出的节点8的右儿子,并将10入栈。stack = [3, 9, 10]index -> inorder[3] = 10
我们遍历
20。同理,index恰好指向当前栈顶节点10,那么我们会依次从栈顶弹出10,9和3,并且将index向右移动了三次。我们将20作为最后弹出的节点3的右儿子,并将20入栈。stack = [20]index -> inorder[6] = 15
我们遍历
15,将15作为栈顶节点20的左儿子,并将15入栈。stack = [20, 15]index -> inorder[6] = 15
我们遍历
7。index恰好指向当前栈顶节点15,那么我们会依次从栈顶弹出15和20,并且将index向右移动了两次。我们将7作为最后弹出的节点20的右儿子,并将7入栈。stack = [7]index -> inorder[8] = 7
此时遍历结束,我们就构造出了正确的二叉树。
算法
我们归纳出上述例子中的算法流程:
我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;
我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果
index恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动index,并将当前节点作为最后一个弹出的节点的右儿子;如果index和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;无论是哪一种情况,我们最后都将当前的节点入栈。
最后得到的二叉树即为答案。
[sol2-C++]class Solution { public: TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { if (!preorder.size()) { return nullptr; } TreeNode* root = new TreeNode(preorder[0]); stack<TreeNode*> stk; stk.push(root); int inorderIndex = 0; for (int i = 1; i < preorder.size(); ++i) { int preorderVal = preorder[i]; TreeNode* node = stk.top(); if (node->val != inorder[inorderIndex]) { node->left = new TreeNode(preorderVal); stk.push(node->left); } else { while (!stk.empty() && stk.top()->val == inorder[inorderIndex]) { node = stk.top(); stk.pop(); ++inorderIndex; } node->right = new TreeNode(preorderVal); stk.push(node->right); } } return root; } };
[sol2-Java]class Solution { public TreeNode buildTree(int[] preorder, int[] inorder) { if (preorder == null || preorder.length == 0) { return null; } TreeNode root = new TreeNode(preorder[0]); Deque<TreeNode> stack = new LinkedList<TreeNode>(); stack.push(root); int inorderIndex = 0; for (int i = 1; i < preorder.length; i++) { int preorderVal = preorder[i]; TreeNode node = stack.peek(); if (node.val != inorder[inorderIndex]) { node.left = new TreeNode(preorderVal); stack.push(node.left); } else { while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) { node = stack.pop(); inorderIndex++; } node.right = new TreeNode(preorderVal); stack.push(node.right); } } return root; } }
[sol2-Python3]class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: if not preorder: return None root = TreeNode(preorder[0]) stack = [root] inorderIndex = 0 for i in range(1, len(preorder)): preorderVal = preorder[i] node = stack[-1] if node.val != inorder[inorderIndex]: node.left = TreeNode(preorderVal) stack.append(node.left) else: while stack and stack[-1].val == inorder[inorderIndex]: node = stack.pop() inorderIndex += 1 node.right = TreeNode(preorderVal) stack.append(node.right) return root
[sol2-Golang]func buildTree(preorder []int, inorder []int) *TreeNode { if len(preorder) == 0 { return nil } root := &TreeNode{preorder[0], nil, nil} stack := []*TreeNode{} stack = append(stack, root) var inorderIndex int for i := 1; i < len(preorder); i++ { preorderVal := preorder[i] node := stack[len(stack)-1] if node.Val != inorder[inorderIndex] { node.Left = &TreeNode{preorderVal, nil, nil} stack = append(stack, node.Left) } else { for len(stack) != 0 && stack[len(stack)-1].Val == inorder[inorderIndex] { node = stack[len(stack)-1] stack = stack[:len(stack)-1] inorderIndex++ } node.Right = &TreeNode{preorderVal, nil, nil} stack = append(stack, node.Right) } } return root }
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 是树中的节点个数。
空间复杂度:$O(n)$,除去返回的答案需要的 $O(n)$ 空间之外,我们还需要使用 $O(h)$(其中 $h$ 是树的高度)的空间存储栈。这里 $h < n$,所以(在最坏情况下)总空间复杂度为 $O(n)$。
统计信息
| 通过次数 | 提交次数 | AC比率 |
|---|---|---|
| 270461 | 383302 | 70.6% |
提交历史
| 提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
|---|
相似题目
| 题目 | 难度 |
|---|---|
| 从中序与后序遍历序列构造二叉树 | 中等 |