Problem: 105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
文章目录
整体思路
这段代码旨在解决一个经典的二叉树构造问题:从前序遍历和中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal)。这是一个典型的分治(Divide and Conquer)算法应用。
算法的核心思想基于前序遍历和中序遍历的根本性质:
- 前序遍历 (Preorder) :
[根节点, [左子树的前序遍历], [右子树的前序遍历]]
- 其第一个元素 永远是当前(子)树的根节点。
- 中序遍历 (Inorder) :
[[左子树的中序遍历], 根节点, [右子树的中序遍历]]
- 根节点会把整个序列分割成两部分:左边是其左子树的所有节点,右边是其右子树的所有节点。
该递归算法的逻辑步骤如下:
-
确定根节点 (Divide):
- 根据前序遍历的性质,
preorder
数组的第一个元素preorder[0]
就是当前树的根节点。
- 根据前序遍历的性质,
-
分割子树 (Divide):
- 在
inorder
数组中找到这个根节点值的位置。这个位置(我们称之为i
)至关重要。 inorder
数组中,索引i
左边的所有元素,构成了根节点的左子树的中序遍历。- 索引
i
右边的所有元素,构成了根节点的右子树的中序遍历。 - 左子树的大小
leftSize
就等于i
。
- 在
-
构建子树的遍历序列 (Divide):
- 根据
leftSize
,我们现在可以从preorder
数组中也分割出左右子树的前序遍历序列。preorder
数组中,从索引1
开始,长度为leftSize
的部分,是左子树的前序遍历。preorder
数组剩下的部分,是右子树的前序遍历。
- 【效率瓶颈】 :此代码通过
Arrays.copyOfRange
来创建四个全新的、更小的数组,分别代表左右子树的前序和中序遍历。这一步虽然逻辑清晰,但涉及大量的数组复制,是性能不佳的主要原因。
- 根据
-
递归构造 (Conquer):
- 将分割出的左子树的前序和中序遍历数组,递归地传入
buildTree
函数,构造出左子节点left
。 - 同样地,将右子树的两个数组传入,构造出右子节点
right
。 - 递归的基线条件是当传入的数组为空时,返回
null
。
- 将分割出的左子树的前序和中序遍历数组,递归地传入
-
合并结果 (Combine):
- 创建一个新的
TreeNode
,其值为preorder[0]
,并将其left
和right
指针分别指向上面递归构造出的左右子节点。 - 返回这个新创建的根节点。
- 创建一个新的
完整代码
java
class Solution {
/**
* 根据前序遍历和中序遍历的结果,构造二叉树。
* @param preorder 前序遍历数组
* @param inorder 中序遍历数组
* @return 构造出的二叉树的根节点
*/
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 递归的基线条件:如果遍历序列为空,则子树也为空。
if (n == 0) {
return null;
}
// 步骤 1: 确定根节点。前序遍历的第一个元素就是根。
// 步骤 2: 在中序遍历中找到根节点的位置,以确定左子树的大小。
int leftSize = 0;
for (int i = 0; i < n; i++) {
if (inorder[i] == preorder[0]) {
leftSize = i;
break; // 找到后即可退出循环
}
}
// 步骤 3: 【效率瓶颈】分割数组,为左右子树创建新的遍历序列。
// 这会创建四个新的数组,导致时间和空间开销较大。
// 获取左子树的前序遍历
int[] pre1 = Arrays.copyOfRange(preorder, 1, 1 + leftSize);
// 获取右子树的前序遍历
int[] pre2 = Arrays.copyOfRange(preorder, 1 + leftSize, n);
// 获取左子树的中序遍历
int[] in1 = Arrays.copyOfRange(inorder, 0, leftSize);
// 获取右子树的中序遍历
int[] in2 = Arrays.copyOfRange(inorder, 1 + leftSize, n);
// 步骤 4: 递归地构建左右子树
TreeNode left = buildTree(pre1, in1);
TreeNode right = buildTree(pre2, in2);
// 步骤 5: 创建根节点,并连接其左右子树
return new TreeNode(preorder[0], left, right);
}
}
时空复杂度
时间复杂度:O(N^2)
- 根节点查找 :在每次
buildTree
调用中(处理一个大小为k
的子问题),for
循环会在中序遍历数组中进行线性扫描以找到根节点。此操作的时间复杂度为 O(k)。 - 数组复制 :
Arrays.copyOfRange
操作需要复制元素。在处理大小为k
的子问题时,总共复制k-1
个元素来创建新的子数组。此操作的时间复杂度也是 O(k)。 - 递归结构 :
- 设
T(N)
为处理N
个节点的时间复杂度。 T(N) = O(N) + T(leftSize) + T(N - 1 - leftSize)
。其中O(N)
是当前层级的查找和复制开销。- 在最坏的情况下(例如,树退化成一个链表),
leftSize
始终为0
或N-1
。此时,递归关系变为T(N) = T(N-1) + O(N)
。 - 展开这个递推式:
T(N) = O(N) + O(N-1) + O(N-2) + ... + O(1)
。这是一个等差数列求和,结果为 O(N^2)。
- 设
综合分析 :由于在递归的每一步都进行了线性的数组扫描和数组复制,该算法的时间复杂度为 O(N^2)。
空间复杂度:O(N^2)
- 递归调用栈 :递归的深度等于树的高度
H
。在最坏情况下(链状树),H=N
,递归栈本身会占用 O(N) 的空间。 - 辅助数组 :这是空间复杂度的主要来源。在递归的每一层,我们都创建了新的数组副本。
- 考虑最坏情况(链状树),在递归深度为
d
时,会创建大小约为N-d
的数组。 - 递归栈上会同时存在多层调用,每一层都持有自己创建的数组。例如,第一层持有大小为
N-1
的数组,第二层持有大小为N-2
的数组,以此类推。 - 峰值空间占用发生在最深的递归路径上,总空间约为
(N-1) + (N-2) + ... + 1
,这也是 O(N^2) 级别。
- 考虑最坏情况(链状树),在递归深度为
综合分析 :由于递归调用中创建了大量的数组副本,导致总的空间消耗非常大。因此,在最坏情况下,空间复杂度为 O(N^2)。
【LeetCode 热题 100】105. 从前序与中序遍历序列构造二叉树------(解法二)O(n)
参考灵神