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 的解法。