题目描述

✅ 449. 序列化和反序列化二叉搜索树

image-20260928235622420

题意分析

将二叉搜索树编码为尽可能紧凑的字符串,再从字符串恢复原来的节点值和结构。利用 BST 的左子树值小于根、右子树值大于根这一性质,可以只记录前序节点值,不必为每个空孩子写占位符。本文用空字符串表示空树。

解法:前序遍历加上下界恢复

核心思路

[!blue]

前序遍历按“根、左子树、右子树”输出,每棵子树的节点在序列中连续出现。普通二叉树仅凭这些值无法知道左右子树的边界,但 BST 的取值范围提供了结构约束:以 value 为根,左边只能取更小的值,右边只能取更大的值。

定义 build(lower, upper) 恢复值必须落在开区间 (lower, upper) 内的子树,用共享下标 index 指向下一个未读取的值。序列耗尽时返回空;若下一个值不在当前范围,说明它不能作为这棵子树的根,当前子树为空,应交还给上层处理,不能推进下标。

值合法时,先将它作为根并推进下标,再按前序顺序恢复左子树 (lower, value) 和右子树 (value, upper)。保留原有另一侧边界,才能同时满足所有祖先的限制,而不只是与直接父节点比较。

对合法 BST 的前序序列,下一项若属于当前子树,就必然是它的根;孩子递归又使用相同规则,返回时正好消耗完整子树。因而左右子树能依次唯一恢复,不需要重复搜索分界位置,也不会把属于后续子树的值提前丢弃。

初始上下界必须严格覆盖题目值域。代码使用 64 位边界,并直接传递根值作为新的开区间端点,无需对根值做加一或减一。每次反序列化重新创建共享下标,解析过程只依赖当前字符串。

解题步骤

  1. 序列化时前序访问所有非空节点,用空格分隔节点值;空树不写任何内容。
  2. 反序列化空字符串时返回空树,否则分割字段,将 index 初始化为 0。
  3. 从覆盖所有合法值的开区间开始调用 build。
  4. 下标到末尾或下一个值越界时返回空,保持下标不变。
  5. 值合法时消费当前字段、创建根节点,然后依次用收紧后的范围恢复左、右子树。

代码实现

public class Codec {
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();

        preorder(root, sb);

        return sb.toString().trim();
    }

    private void preorder(TreeNode node, StringBuilder sb) {
        if (node == null) {
            return;
        }

        sb.append(node.val).append(' ');
        preorder(node.left, sb);
        preorder(node.right, sb);
    }

    public TreeNode deserialize(String data) {
        if (data == null || data.isEmpty()) {
            return null;
        }

        String[] values = data.split(" ");
        int[] index = new int[1];

        return build(values, index, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    private TreeNode build(String[] values, int[] index, long lower, long upper) {
        if (index[0] == values.length) {
            return null;
        }

        int value = Integer.parseInt(values[index[0]]);

        // 越界值属于其他子树,返回时不能消费它
        if (value <= lower || value >= upper) {
            return null;
        }

        // 合法根先消费,再让左右子树继续共享读取位置
        index[0]++;
        TreeNode node = new TreeNode(value);

        node.left = build(values, index, lower, value);
        node.right = build(values, index, value, upper);

        return node;
    }
}
import (
    "strconv"
    "strings"
)

type Codec struct {
}

func Constructor() Codec {
    return Codec{}
}

func (this *Codec) serialize(root *TreeNode) string {
    parts := []string{}
    var preorder func(*TreeNode)
    preorder = func(node *TreeNode) {
        if node == nil {
            return
        }
        parts = append(parts, strconv.Itoa(node.Val))
        preorder(node.Left)
        preorder(node.Right)
    }
    preorder(root)
    return strings.Join(parts, " ")
}

func (this *Codec) deserialize(data string) *TreeNode {
    if data == "" {
        return nil
    }
    values := strings.Fields(data)
    index := 0

    var build func(int64, int64) *TreeNode
    build = func(lower int64, upper int64) *TreeNode {
        if index == len(values) {
            return nil
        }
        value, _ := strconv.Atoi(values[index])
        current := int64(value)
        // 越界值属于其他子树,返回时不能消费它
        if current <= lower || current >= upper {
            return nil
        }

        // 合法根先消费,再让左右子树继续共享读取位置
        index++
        node := &TreeNode{Val: value}
        node.Left = build(lower, current)
        node.Right = build(current, upper)
        return node
    }

    return build(-1<<63, 1<<63-1)
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是节点数。编码访问每个节点一次;解码时每个节点创建一次,并发起两次孩子调用,所以包含空子树判断在内的调用总数也是线性的。
  • 空间复杂度:$O(n)$,编码字符串、解码分词和结果树都占线性空间;递归栈另外占 $O(h)$,其中 $h$ 为树高,树退化成链时为 $O(n)$。

关键点总结

[!green]

  • 前序决定读取顺序,BST 的祖先上下界决定每个子树的范围,两者共同替代空节点标记。
  • 只有成功创建节点时才推进下标,越界返回空不会丢弃尚未分配的值。
  • 左右递归保留继承边界,始终满足整条祖先路径的 BST 约束。

易错点总结

[!yellow]

  • 先递增下标再检查范围,会跳过属于其他子树的节点。
  • 只比较父节点、丢掉原有另一侧边界,会把祖先另一分支的节点接到当前子树。
  • 先恢复右子树会破坏前序的“根、左、右”读取顺序。
  • 范围是开区间,初始边界不能等于某个合法节点值,否则这个值会被误判越界。
  • 本方法依赖输入来自合法 BST 的前序编码,不能直接套用到普通二叉树的前序序列。

相似题目

题目 难度 关联与区别
297. 二叉树的序列化与反序列化 困难 普通二叉树需要保存空位结构,BST可利用值域上下界从前序序列还原。
255. 验证二叉搜索树的前序遍历序列 中等 BST前序验证与还原都依赖进入右子树后的下界,本题还要实际创建节点。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/57808794
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!