LeetCode第105题:从前序与中序遍历序列构造二叉树
题目描述
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的前序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
难度
中等
问题链接
示例
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder和inorder均无重复元素inorder均出现在preorderpreorder保证为二叉树的前序遍历序列inorder保证为二叉树的中序遍历序列
解题思路
这道题要求我们根据二叉树的前序遍历和中序遍历序列来构造二叉树。前序遍历的顺序是:根节点、左子树、右子树;中序遍历的顺序是:左子树、根节点、右子树。
我们可以利用这两种遍历方式的特点来构造二叉树:
- 前序遍历的第一个元素是根节点。
- 在中序遍历中,根节点将数组分成两部分:左子树和右子树。
- 通过在中序遍历中找到根节点的位置,我们可以确定左子树和右子树的节点数量。
- 然后,我们可以递归地构造左子树和右子树。
方法一:递归
我们可以使用递归的方式来构造二叉树:
- 从前序遍历中找到根节点(第一个元素)。
- 在中序遍历中找到根节点的位置,将数组分成左子树和右子树。
- 递归地构造左子树和右子树。
方法二:迭代
我们也可以使用迭代的方式来构造二叉树,但这种方法相对复杂,不如递归直观。
算法步骤分析
递归方法:
- 如果前序遍历数组为空,返回
null。 - 取前序遍历的第一个元素作为根节点。
- 在中序遍历中找到根节点的位置,将数组分成左子树和右子树。
- 递归地构造左子树:
- 前序遍历的子数组:从索引 1 开始,长度为左子树的节点数量。
- 中序遍历的子数组:从开始到根节点位置前一个元素。
- 递归地构造右子树:
- 前序遍历的子数组:从索引 1 + 左子树节点数量开始,到结束。
- 中序遍历的子数组:从根节点位置后一个元素到结束。
- 返回根节点。
优化:使用哈希表
为了提高在中序遍历中查找根节点位置的效率,我们可以使用哈希表来存储中序遍历中每个元素的索引。这样,我们可以在 O(1) 的时间内找到根节点的位置,而不是 O(n) 的时间。
算法可视化
以示例 1 为例,preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]:
- 根节点是
preorder[0] = 3。 - 在
inorder中找到3的位置,是索引 1。 - 左子树的中序遍历是
inorder[0:1] = [9],右子树的中序遍历是inorder[2:] = [15,20,7]。 - 左子树的前序遍历是
preorder[1:2] = [9],右子树的前序遍历是preorder[2:] = [20,15,7]。 - 递归构造左子树:
- 根节点是
preorder[1] = 9。 - 在
inorder[0:1]中找到9的位置,是索引 0。 - 左子树和右子树都为空。
- 根节点是
- 递归构造右子树:
- 根节点是
preorder[2] = 20。 - 在
inorder[2:]中找到20的位置,是索引 1(相对于inorder[2:])。 - 左子树的中序遍历是
inorder[2:3] = [15],右子树的中序遍历是inorder[4:] = [7]。 - 左子树的前序遍历是
preorder[3:4] = [15],右子树的前序遍历是preorder[4:] = [7]。 - 递归构造左子树和右子树。
- 根节点是
- 最终构造的二叉树是
[3,9,20,null,null,15,7]。
代码实现
C#
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
public TreeNode BuildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.Length == 0) {
return null;
}
// 创建哈希表,存储中序遍历中每个元素的索引
Dictionary<int, int> inorderMap = new Dictionary<int, int>();
for (int i = 0; i < inorder.Length; i++) {
inorderMap[inorder[i]] = i;
}
return BuildTreeHelper(preorder, 0, preorder.Length - 1, inorder, 0, inorder.Length - 1, inorderMap);
}
private TreeNode BuildTreeHelper(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Dictionary<int, int> inorderMap) {
if (preStart > preEnd || inStart > inEnd) {
return null;
}
// 前序遍历的第一个元素是根节点
int rootVal = preorder[preStart];
TreeNode root = new TreeNode(rootVal);
// 在中序遍历中找到根节点的位置
int rootIndex = inorderMap[rootVal];
// 计算左子树的节点数量
int leftSubtreeSize = rootIndex - inStart;
// 递归构造左子树
root.left = BuildTreeHelper(preorder, preStart + 1, preStart + leftSubtreeSize, inorder, inStart, rootIndex - 1, inorderMap);
// 递归构造右子树
root.right = BuildTreeHelper(preorder, preStart + leftSubtreeSize + 1, preEnd, inorder, rootIndex + 1, inEnd, inorderMap);
return root;
}
}
Python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder:
return None
# 创建哈希表,存储中序遍历中每个元素的索引
inorder_map = {val: idx for idx, val in enumerate(inorder)}
def build_tree_helper(pre_start, pre_end, in_start, in_end):
if pre_start > pre_end or in_start > in_end:
return None
# 前序遍历的第一个元素是根节点
root_val = preorder[pre_start]
root = TreeNode(root_val)
# 在中序遍历中找到根节点的位置
root_index = inorder_map[root_val]
# 计算左子树的节点数量
left_subtree_size = root_index - in_start
# 递归构造左子树
root.left = build_tree_helper(pre_start + 1, pre_start + left_subtree_size, in_start, root_index - 1)
# 递归构造右子树
root.right = build_tree_helper(pre_start + left_subtree_size + 1, pre_end, root_index + 1, in_end)
return root
return build_tree_helper(0, len(preorder) - 1, 0, len(inorder) - 1)
C++
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) {
return nullptr;
}
// 创建哈希表,存储中序遍历中每个元素的索引
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
return buildTreeHelper(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1, inorderMap);
}
private:
TreeNode* buildTreeHelper(vector<int>& preorder, int preStart, int preEnd, vector<int>& inorder, int inStart, int inEnd, unordered_map<int, int>& inorderMap) {
if (preStart > preEnd || inStart > inEnd) {
return nullptr;
}
// 前序遍历的第一个元素是根节点
int rootVal = preorder[preStart];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍历中找到根节点的位置
int rootIndex = inorderMap[rootVal];
// 计算左子树的节点数量
int leftSubtreeSize = rootIndex - inStart;
// 递归构造左子树
root->left = buildTreeHelper(preorder, preStart + 1, preStart + leftSubtreeSize, inorder, inStart, rootIndex - 1, inorderMap);
// 递归构造右子树
root->right = buildTreeHelper(preorder, preStart + leftSubtreeSize + 1, preEnd, inorder, rootIndex + 1, inEnd, inorderMap);
return root;
}
};
执行结果
C#
- 执行用时:84 ms,击败了 95.65% 的 C# 提交
- 内存消耗:40.1 MB,击败了 91.30% 的 C# 提交
Python
- 执行用时:48 ms,击败了 94.12% 的 Python3 提交
- 内存消耗:18.6 MB,击败了 88.24% 的 Python3 提交
C++
- 执行用时:12 ms,击败了 96.30% 的 C++ 提交
- 内存消耗:25.9 MB,击败了 90.74% 的 C++ 提交
代码亮点
- 哈希表优化:使用哈希表存储中序遍历中每个元素的索引,将查找根节点位置的时间复杂度从 O(n) 降低到 O(1)。
- 递归构造:通过递归的方式构造二叉树,代码结构清晰,易于理解。
- 边界条件处理:正确处理了边界条件,如空数组和子树为空的情况。
- 索引计算:通过计算左子树的节点数量,准确地划分了前序遍历和中序遍历的子数组。
常见错误分析
- 索引计算错误:在划分前序遍历和中序遍历的子数组时,容易出现索引计算错误,导致构造的二叉树不正确。
- 忽略边界条件:忘记检查数组是否为空,或者子树的范围是否有效,可能导致数组越界或空指针异常。
- 未使用哈希表优化:直接在中序遍历中线性查找根节点的位置,导致时间复杂度增加。
- 递归终止条件错误:递归终止条件不正确,可能导致无限递归或构造不完整的二叉树。
解法比较
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归 + 哈希表 | O(n) | O(n) | 实现简单,时间复杂度低 | 需要额外的空间存储哈希表 |
| 递归(不使用哈希表) | O(n²) | O(n) | 实现简单,不需要额外的哈希表 | 时间复杂度高,每次都需要线性查找根节点位置 |
| 迭代 | O(n) | O(n) | 避免了递归可能导致的栈溢出问题 | 实现复杂,不如递归直观 |