105. 从前序与中序遍历序列构造二叉树
问题描述
给定一棵二叉树的前序遍历 preorder
和中序遍历 inorder
,构造并返回二叉树。假设树中无重复元素。
示例:
输入:preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出:[3,9,20,null,null,15,7]
算法思路
递归分治法
:
定位根节点
:
前序遍历的首元素即为根节点值。划分左右子树
:- 在中序遍历中找到根节点位置,左侧为左子树中序序列,右侧为右子树中序序列。
- 根据左子树长度,在前序遍历中划分左子树前序序列和右子树前序序列。
递归构建
:- 用左子树的前序+中序序列构建左子树。
- 用右子树的前序+中序序列构建右子树。
优化:
使用哈希表
存储中序遍历的值-索引映射,将根节点查找时间优化至 O(1)。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 存储中序遍历的值到索引的映射
private Map<Integer, Integer> inorderMap;
// 全局前序遍历数组
private int[] preorder;
// 前序遍历索引指针
private int preIndex;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
this.inorderMap = new HashMap<>();
this.preIndex = 0;
// 构建中序遍历哈希表
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
return buildSubTree(0, inorder.length - 1);
}
/**
* 递归构建子树
*
* @param inStart 中序序列起始索引
* @param inEnd 中序序列结束索引
* @return 构建好的子树根节点
*/
private TreeNode buildSubTree(int inStart, int inEnd) {
// 终止条件:中序序列为空
if (inStart > inEnd) return null;
// 创建根节点(前序遍历当前元素)
int rootValue = preorder[preIndex++];
TreeNode root = new TreeNode(rootValue);
// 获取根节点在中序序列中的位置
int rootIndex = inorderMap.get(rootValue);
// 递归构建左子树(中序序列根节点左侧)
root.left = buildSubTree(inStart, rootIndex - 1);
// 递归构建右子树(中序序列根节点右侧)
root.right = buildSubTree(rootIndex + 1, inEnd);
return root;
}
}
// TreeNode定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
代码注释
代码部分 | 说明 |
---|---|
inorderMap | 存储中序遍历值到索引的映射 |
preIndex | 前序遍历索引指针(全局位置) |
buildTree() | 初始化并启动递归构建 |
buildSubTree(inStart, inEnd) | 递归构建子树 |
preorder[preIndex++] | 创建根节点并前移指针 |
inorderMap.get(rootValue) | 获取根节点在中序序列的位置 |
buildSubTree(inStart, rootIndex-1) | 构建左子树 |
buildSubTree(rootIndex+1, inEnd) | 构建右子树 |
算法过程
preorder = [3,9,20,15,7]
, inorder = [9,3,15,20,7]
:
- 初始状态:
preIndex=0
,inStart=0
,inEnd=4
- 根节点:
rootValue=3
→rootIndex=1
(在inorder
中的位置) - 左子树构建:
- 中序序列:
inStart=0
,inEnd=0
([9]
) - 创建节点
9
(preIndex=1
)
- 中序序列:
- 右子树构建:
- 中序序列:
inStart=2
,inEnd=4
([15,20,7]
) - 根节点
20
(preIndex=2
) - 左子树:
15
(preIndex=3
) - 右子树:
7
(preIndex=4
)
- 中序序列:
复杂度分析
- 时间复杂度:O(n)
每个节点处理一次,哈希表查找 O(1) - 空间复杂度:O(n)
哈希表 O(n),递归栈深度 O(h)(h 为树高)
关键点
- 前序首元素为根:
前序遍历的首元素是当前子树的根节点。 - 中序定位左右子树:
中序遍历中根节点左侧为左子树,右侧为右子树。 - 递归构建子树:
根据划分的左右子树序列递归构建。 - 哈希表优化:
避免每次线性查找根节点位置。 - 指针控制:
使用全局preIndex
按前序顺序处理节点。
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 示例测试
int[] pre1 = {3,9,20,15,7};
int[] in1 = {9,3,15,20,7};
TreeNode root1 = solution.buildTree(pre1, in1);
printTree(root1); // [3,9,20,null,null,15,7]
// 单节点测试
int[] pre2 = {1};
int[] in2 = {1};
TreeNode root2 = solution.buildTree(pre2, in2);
printTree(root2); // [1]
// 左斜树测试
int[] pre3 = {1,2,3};
int[] in3 = {3,2,1};
TreeNode root3 = solution.buildTree(pre3, in3);
printTree(root3); // [1,2,null,3]
// 右斜树测试
int[] pre4 = {1,2,3};
int[] in4 = {1,2,3};
TreeNode root4 = solution.buildTree(pre4, in4);
printTree(root4); // [1,null,2,null,3]
}
// 辅助方法:打印二叉树(层序遍历)
private static void printTree(TreeNode root) {
if (root == null) {
System.out.println("[]");
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
List<String> result = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node != null) {
result.add(String.valueOf(node.val));
queue.add(node.left);
queue.add(node.right);
} else {
result.add("null");
}
}
// 去除末尾多余的null
int i = result.size() - 1;
while (i >= 0 && result.get(i).equals("null")) {
i--;
}
System.out.println(result.subList(0, i + 1));
}
常见问题
-
为什么需要哈希表?
哈希表将中序遍历的根节点查找从 O(n) 优化至 O(1),显著提升性能。 -
如何处理空子树?
当inStart > inEnd
时返回null
,作为递归终止条件。 -
为什么使用全局
preIndex
?
前序遍历序列需按顺序处理,全局指针确保每个节点只处理一次。 -
能否处理重复元素?
题目假设无重复元素,若有重复元素需额外处理(如返回任意有效树)。