LEETCODE 105Medium
从前序与中序遍历序列构造二叉树
前序的第一个元素永远是根,拿它去中序里一切两半,左右子树的边界就都确定了——递归只传下标区间,不必真的切数组。
问题拆解
给出一棵树的前序遍历和中序遍历(节点值互不相同),还原这棵树。
两个序列各自藏着不同的信息:前序是“根 → 左子树 → 右子树”,所以它的第一个元素一定是整棵树的根;中序是“左子树 → 根 → 右子树”,所以只要在中序里找到根的位置,它左边的就是左子树的全部节点,右边的就是右子树的全部节点。左子树的节点个数一旦知道,前序序列也能跟着切开——根后面紧跟的那一段就是左子树的前序。
于是问题递归地变小了:用同样的办法分别构造左右子树。两个容易写慢的地方:一是每层都线性扫描中序找根,整体退化到 O(n²),应当先用哈希表把“值 → 中序下标”存好;二是每层都真的切出新数组,既费拷贝又容易把边界切错——递归函数只传下标区间就够了。
前序定根,中序分左右;左子树的“个数”是打通两个序列的桥梁。
哈希定位 + 下标区间递归
递归函数带四个下标:当前子树在前序中的区间 [preLeft, preRight] 和在中序中的区间 [inLeft, inRight]。设根在中序中的位置为 mid,则左子树大小 leftSize = mid - inLeft,两个序列的切分点随之确定。
private Map<Integer, Integer> pos = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) {
pos.put(inorder[i], i); // 值 -> 中序下标
}
return build(preorder, 0, preorder.length - 1, 0, inorder.length - 1);
}
private TreeNode build(int[] preorder, int preLeft, int preRight, int inLeft, int inRight) {
if (preLeft > preRight) {
return null;
}
int mid = pos.get(preorder[preLeft]); // 根在中序中的位置
int leftSize = mid - inLeft;
TreeNode root = new TreeNode(preorder[preLeft]);
root.left = build(preorder, preLeft + 1, preLeft + leftSize, inLeft, mid - 1);
root.right = build(preorder, preLeft + leftSize + 1, preRight, mid + 1, inRight);
return root;
}
def buildTree(preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
pos = {v: i for i, v in enumerate(inorder)} # 值 -> 中序下标
def build(pre_left: int, pre_right: int, in_left: int, in_right: int) -> Optional[TreeNode]:
if pre_left > pre_right:
return None
mid = pos[preorder[pre_left]] # 根在中序中的位置
left_size = mid - in_left
root = TreeNode(preorder[pre_left])
root.left = build(pre_left + 1, pre_left + left_size, in_left, mid - 1)
root.right = build(pre_left + left_size + 1, pre_right, mid + 1, in_right)
return root
return build(0, len(preorder) - 1, 0, len(inorder) - 1)
func buildTree(preorder []int, inorder []int) *TreeNode {
pos := make(map[int]int, len(inorder)) // 值 -> 中序下标
for i, v := range inorder {
pos[v] = i
}
var build func(preLeft, preRight, inLeft, inRight int) *TreeNode
build = func(preLeft, preRight, inLeft, inRight int) *TreeNode {
if preLeft > preRight {
return nil
}
mid := pos[preorder[preLeft]] // 根在中序中的位置
leftSize := mid - inLeft
root := &TreeNode{Val: preorder[preLeft]}
root.Left = build(preLeft+1, preLeft+leftSize, inLeft, mid-1)
root.Right = build(preLeft+leftSize+1, preRight, mid+1, inRight)
return root
}
return build(0, len(preorder)-1, 0, len(inorder)-1)
}
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
pub fn build_tree(preorder: Vec<i32>, inorder: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> {
// 值 -> 中序下标
let pos: HashMap<i32, usize> = inorder.iter().enumerate().map(|(i, &v)| (v, i)).collect();
// 用 isize 区间,空区间时右端点会比左端点小 1
fn build(
preorder: &[i32],
pos: &HashMap<i32, usize>,
pre_left: isize, pre_right: isize,
in_left: isize, in_right: isize,
) -> Option<Rc<RefCell<TreeNode>>> {
if pre_left > pre_right {
return None;
}
let root_val = preorder[pre_left as usize];
let mid = pos[&root_val] as isize; // 根在中序中的位置
let left_size = mid - in_left;
let root = Rc::new(RefCell::new(TreeNode::new(root_val)));
root.borrow_mut().left =
build(preorder, pos, pre_left + 1, pre_left + left_size, in_left, mid - 1);
root.borrow_mut().right =
build(preorder, pos, pre_left + left_size + 1, pre_right, mid + 1, in_right);
Some(root)
}
let n = preorder.len() as isize;
build(&preorder, &pos, 0, n - 1, 0, n - 1)
}
最容易错的是前序区间的切分:左子树占的是 [preLeft + 1, preLeft + leftSize],右子树从 preLeft + leftSize + 1 开始——这里的加一减一全靠 leftSize 这个量来对齐,建议在纸上用一个五节点的小例子核一遍再提交。Rust 版特意用 isize 做下标,因为空区间时右端点会退到左端点左边,usize 减法会直接 panic。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
哈希表使每个节点的定位是 O(1),每个节点只构造一次 |
| 空间 | O(n) |
哈希表存 n 个下标;递归栈最坏(链状树)也是 O(n) |
可以迁移的模式
- 遍历序列各有分工:前序/后序提供“根在哪”,中序提供“左右怎么分”,组合信息才能唯一还原树;
- 递归处理子数组时传下标区间而不是切片,省拷贝也省心;
- 需要反复“按值找位置”时,先一次性建好哈希表,把每次 O(n) 查找摊成 O(1)。
把这套区间切分改成“后序末元素定根”,就是姊妹题 106 的解法。