cswork / 模板打开 500 路题单

第 12 站 · 链表与树

二叉树构造与变换

前序定根,中序分左右 时间 O(n) · 空间 O(n)

周赛难度分 难度分 1511–难度分 1511· 中位 难度分 1511(1/6 道题有 zerotrac 周赛分)

打开 500 路题单 · 第 12 站 · 6 题
先学
学完去
没有后续站依赖它。
面试频次
3 家公司考过 · 面经出现 4 次NVIDIA 2、StackAdapt 1、TikTok / ByteDance 1

先做这道

LC 105从前序与中序遍历序列构造二叉树中等
卡住再看

由前序确定根、中序划分子树,维护索引边界。

要点

  1. 前序的第一个值是根;在中序里找到它,左边是左子树,右边是右子树。
  2. 用哈希表存「值 → 中序下标」,每次切分 O(1),整体 O(n)。
  3. 按「先左后右」递归时,前序指针顺序往后取就是下一个根,不用计算子数组长度;后序 + 中序则从后往前取、先右后左。

易错点

  • 值有重复时中序位置不唯一,这个方法就不成立了。
  • 每层递归都切片复制数组(Python 切片、Java copyOfRange),会退化成 O(n²)。
  • 后序 + 中序时如果仍然先建左子树,取到的根的顺序就错了。

看动画

一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。

交互演示 · 逐步观察

二叉树遍历 · 栈与队列

前序、中序、后序、层序遍历这棵树

换个输入
ABCDEFG
当前处理已发现 / 已计算答案
栈底 → 栈顶,或队首 → 队尾
空
遍历
前序
输出
—

同一棵树,四种遍历顺序。节点下的 #k 是它在当前遍历里第几个输出。

始终成立深度优先的三种顺序只差在「输出根」排在左右子树的前、中、后;层序用队列,先进先出就是一层一层。

模板

选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 105。

Python
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def build_tree(preorder: list[int], inorder: list[int]):
    """Rebuild a tree with distinct values from its preorder and inorder walks."""
    pos = {v: i for i, v in enumerate(inorder)}  # value -> inorder index: O(1) splits
    roots = iter(preorder)  # preorder yields roots in exactly the order we build them

    def build(lo: int, hi: int):  # the subtree whose inorder range is [lo, hi)
        if lo == hi:
            return None
        root = TreeNode(next(roots))
        mid = pos[root.val]
        root.left = build(lo, mid)  # left before right, matching preorder
        root.right = build(mid + 1, hi)
        return root

    return build(0, len(inorder))
C++
struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int v, TreeNode* l = nullptr, TreeNode* r = nullptr) : val(v), left(l), right(r) {}
};

// Rebuild a tree with distinct values from its preorder and inorder walks.
TreeNode* buildTree(const vector<int>& preorder, const vector<int>& inorder) {
    unordered_map<int, int> pos;  // value -> inorder index: O(1) splits
    for (int i = 0; i < (int)inorder.size(); i++) pos[inorder[i]] = i;
    int next = 0;  // preorder yields roots in exactly the order we build them
    function<TreeNode*(int, int)> build = [&](int lo, int hi) -> TreeNode* {  // inorder [lo, hi)
        if (lo == hi) return nullptr;
        TreeNode* root = new TreeNode(preorder[next++]);
        int mid = pos[root->val];
        root->left = build(lo, mid);  // left before right, matching preorder
        root->right = build(mid + 1, hi);
        return root;
    };
    return build(0, inorder.size());
}
Java
class TreeNode {
    int val;
    TreeNode left, right;

    TreeNode(int val) {
        this.val = val;
    }

    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    private int[] preorder;
    private int next; // preorder yields roots in exactly the order we build them
    private final Map<Integer, Integer> pos = new HashMap<>(); // value -> inorder index

    // Rebuild a tree with distinct values from its preorder and inorder walks.
    TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        next = 0;
        pos.clear();
        for (int i = 0; i < inorder.length; i++) pos.put(inorder[i], i);
        return build(0, inorder.length);
    }

    private TreeNode build(int lo, int hi) { // the subtree whose inorder range is [lo, hi)
        if (lo == hi) return null;
        TreeNode root = new TreeNode(preorder[next++]);
        int mid = pos.get(root.val);
        root.left = build(lo, mid); // left before right, matching preorder
        root.right = build(mid + 1, hi);
        return root;
    }
}
Go
type TreeNode struct {
	Val         int
	Left, Right *TreeNode
}

// buildTree rebuilds a tree with distinct values from its preorder and inorder walks.
func buildTree(preorder, inorder []int) *TreeNode {
	pos := make(map[int]int, len(inorder)) // value -> inorder index: O(1) splits
	for i, v := range inorder {
		pos[v] = i
	}
	next := 0                            // preorder yields roots in exactly the order we build them
	var build func(lo, hi int) *TreeNode // the subtree whose inorder range is [lo, hi)
	build = func(lo, hi int) *TreeNode {
		if lo == hi {
			return nil
		}
		root := &TreeNode{Val: preorder[next]}
		next++
		mid := pos[root.Val]
		root.Left = build(lo, mid) // left before right, matching preorder
		root.Right = build(mid+1, hi)
		return root
	}
	return build(0, len(inorder))
}

常见变体

这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。

中序 + 后序建树:倒着读后序时间 O(n) · 空间 O(n)

给的是中序和后序(LC 106):后序倒过来读就是「根、右、左」,所以每次取下一个根以后要先建右子树、再建左子树;中序里根的位置照样用哈希表 O(1) 找到,把区间切成两半。

Python
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def build_from_post(inorder: list[int], postorder: list[int]):
    """Rebuild a tree with distinct values from its inorder and postorder walks (LC 106)."""
    pos = {v: i for i, v in enumerate(inorder)}  # value -> inorder index
    roots = reversed(postorder)  # backwards, postorder yields root, then right, then left

    def build(lo: int, hi: int):  # the subtree whose inorder range is [lo, hi)
        if lo == hi:
            return None
        root = TreeNode(next(roots))
        mid = pos[root.val]
        root.right = build(mid + 1, hi)  # right first, matching the reversed postorder
        root.left = build(lo, mid)
        return root

    return build(0, len(inorder))
C++
struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int v, TreeNode* l = nullptr, TreeNode* r = nullptr) : val(v), left(l), right(r) {}
};

// Rebuild a tree with distinct values from its inorder and postorder walks (LC 106).
TreeNode* buildFromPost(const vector<int>& inorder, const vector<int>& postorder) {
    unordered_map<int, int> pos;  // value -> inorder index
    for (int i = 0; i < (int)inorder.size(); i++) pos[inorder[i]] = i;
    int next = (int)postorder.size() - 1;  // backwards, postorder yields root, then right, then left
    function<TreeNode*(int, int)> build = [&](int lo, int hi) -> TreeNode* {  // inorder range [lo, hi)
        if (lo == hi) return nullptr;
        TreeNode* root = new TreeNode(postorder[next--]);
        int mid = pos[root->val];
        root->right = build(mid + 1, hi);  // right first, matching the reversed postorder
        root->left = build(lo, mid);
        return root;
    };
    return build(0, inorder.size());
}
Java
class TreeNode {
    int val;
    TreeNode left, right;

    TreeNode(int val) {
        this.val = val;
    }

    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    private final Map<Integer, Integer> pos = new HashMap<>(); // value -> inorder index
    private int[] postorder;
    private int next;

    // Rebuild a tree with distinct values from its inorder and postorder walks (LC 106).
    TreeNode buildFromPost(int[] inorder, int[] postorder) {
        pos.clear();
        for (int i = 0; i < inorder.length; i++) pos.put(inorder[i], i);
        this.postorder = postorder;
        next = postorder.length - 1; // backwards, postorder yields root, then right, then left
        return build(0, inorder.length);
    }

    // The subtree whose inorder range is [lo, hi).
    private TreeNode build(int lo, int hi) {
        if (lo == hi) return null;
        TreeNode root = new TreeNode(postorder[next--]);
        int mid = pos.get(root.val);
        root.right = build(mid + 1, hi); // right first, matching the reversed postorder
        root.left = build(lo, mid);
        return root;
    }
}
Go
type TreeNode struct {
	Val         int
	Left, Right *TreeNode
}

// buildFromPost rebuilds a tree with distinct values from its inorder and postorder walks (LC 106).
func buildFromPost(inorder, postorder []int) *TreeNode {
	pos := map[int]int{} // value -> inorder index
	for i, v := range inorder {
		pos[v] = i
	}
	next := len(postorder) - 1 // backwards, postorder yields root, then right, then left
	var build func(lo, hi int) *TreeNode // the subtree whose inorder range is [lo, hi)
	build = func(lo, hi int) *TreeNode {
		if lo == hi {
			return nil
		}
		root := &TreeNode{Val: postorder[next]}
		next--
		mid := pos[root.Val]
		root.Right = build(mid+1, hi) // right first, matching the reversed postorder
		root.Left = build(lo, mid)
		return root
	}
	return build(0, len(inorder))
}
单调栈一遍建最大二叉树时间 O(n) · 空间 O(n)

「每个子树的根是区间最大值」这类建树(LC 654 最大二叉树,也就是笛卡尔树):递归每次找最大值是 O(n²);用一个值递减的栈只要 O(n),新结点把比它小的结点弹出挂成左子树,自己挂到栈顶更大的结点右边。

Python
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def max_tree(nums: list[int]):
    """Maximum binary tree of distinct nums (LC 654), built in O(n)."""
    stack = []  # the right spine of the tree so far, values decreasing
    for x in nums:
        node = TreeNode(x)
        while stack and stack[-1].val < x:
            node.left = stack.pop()  # smaller nodes before x end up in its left subtree
        if stack:
            stack[-1].right = node  # x hangs right of the nearest larger value before it
        stack.append(node)
    return stack[0] if stack else None
C++
struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int v, TreeNode* l = nullptr, TreeNode* r = nullptr) : val(v), left(l), right(r) {}
};

// Maximum binary tree of distinct nums (LC 654), built in O(n).
TreeNode* maxTree(const vector<int>& nums) {
    vector<TreeNode*> stack;  // the right spine of the tree so far, values decreasing
    for (int x : nums) {
        TreeNode* node = new TreeNode(x);
        while (!stack.empty() && stack.back()->val < x) {
            node->left = stack.back();  // smaller nodes before x end up in its left subtree
            stack.pop_back();
        }
        if (!stack.empty()) stack.back()->right = node;  // x hangs right of the nearest larger value
        stack.push_back(node);
    }
    return stack.empty() ? nullptr : stack[0];
}
Java
class TreeNode {
    int val;
    TreeNode left, right;

    TreeNode(int val) {
        this.val = val;
    }

    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    // Maximum binary tree of distinct nums (LC 654), built in O(n).
    TreeNode maxTree(int[] nums) {
        Deque<TreeNode> stack = new ArrayDeque<>(); // the right spine of the tree so far, values decreasing
        for (int x : nums) {
            TreeNode node = new TreeNode(x);
            while (!stack.isEmpty() && stack.peek().val < x)
                node.left = stack.pop(); // smaller nodes before x end up in its left subtree
            if (!stack.isEmpty()) stack.peek().right = node; // x hangs right of the nearest larger value
            stack.push(node);
        }
        return stack.isEmpty() ? null : stack.peekLast();
    }
}
Go
type TreeNode struct {
	Val         int
	Left, Right *TreeNode
}

// maxTree builds the maximum binary tree of distinct nums (LC 654) in O(n).
func maxTree(nums []int) *TreeNode {
	var stack []*TreeNode // the right spine of the tree so far, values decreasing
	for _, x := range nums {
		node := &TreeNode{Val: x}
		for len(stack) > 0 && stack[len(stack)-1].Val < x {
			node.Left = stack[len(stack)-1] // smaller nodes before x end up in its left subtree
			stack = stack[:len(stack)-1]
		}
		if len(stack) > 0 {
			stack[len(stack)-1].Right = node // x hangs right of the nearest larger value
		}
		stack = append(stack, node)
	}
	if len(stack) == 0 {
		return nil
	}
	return stack[0]
}

为什么对

  1. 先序的第一个值一定是根。值互不相同时,它在中序里的位置唯一,左边恰好是左子树的全部节点、右边是右子树,于是左右子树的大小也确定了,递归下去重建出的树唯一。
  2. 先序是「根、整个左子树、整个右子树」,所以先递归左子树、再递归右子树时,用一个全局指针顺序读先序,读到的总是下一个子树的根。
  3. 值有重复时,根在中序里的位置不唯一,树也可能不唯一,这就是题目保证值互不相同的原因。

30 秒口述稿

面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。

中文
先序的第一个值是根,我在中序里找到它,左边是左子树、右边是右子树,然后递归构造。为了 O(1) 定位,先建一个值到中序下标的哈希表。因为先序是根、左子树、右子树的顺序,只要先建左再建右,用一个指针顺序读先序就总能拿到下一个根。整体 O(n),前提是值互不相同。
English
The first preorder value is the root; I find it in the inorder array, where everything to its left is the left subtree and everything to its right is the right subtree, and recurse. A map from value to inorder index makes each lookup O(1). Because preorder lists root, left subtree, right subtree, building left before right lets one pointer read preorder in order and always get the next root. The build is O(n), given distinct values.

自评一下

对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。

对照要点(0/3)
这道题你会了吗?

小测

3 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习

3 道题
  1. 节点值互不相同时,下面哪些遍历组合能唯一确定一棵二叉树?(多选)
  2. 由前序和中序重建二叉树时,先建一个「值 → 中序下标」的哈希表,带来的改变是?
  3. 把 n 个互不相同的有序数建成一棵高度平衡的 BST(递归时传下标区间,不切片)。每个子区间选谁当根,总时间是多少?

作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。

模板练习

用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。

下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


    return build(0, len(inorder))

分档练习

本站在 500 路里的 6 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。

核心 5

#题目一句话思路难度分
105从前序与中序遍历序列构造二叉树由前序确定根、中序划分子树,维护索引边界。
106从中序与后序遍历序列构造二叉树由后序和中序重建树,练习不同遍历顺序的区间递归。
654最大二叉树按最大值递归构树,并认识单调栈的线性构造优化。
114二叉树展开为链表将树原地展开为前序链表,正确衔接左子树尾部与原右子树。
1110删点成林删除节点并返回森林,区分当前节点保留与子节点成为新根。难度分 1511

进阶 1

#题目一句话思路难度分
652寻找重复的子树为子树结构建立可比较的标识,检测重复子树而非重复节点值。