题目描述

✅ 331. 验证二叉树的前序序列化

image-20260928234148379

image-20260928234148380

题意分析

前序序列依次记录根、左子树和右子树,空节点用 # 占位。题目保证逗号分隔的每一项已经是合法整数或 #,只需判断这些项能否组成完整的二叉树,并且不能重建树。

解法:槽位计数验证序列化

核心思路

[!blue]

把一个等待填写节点或 # 的位置称为槽位,用 slots 记录尚未填写的槽位数量。初始只有根的位置,所以 slots = 1。每读入一项,都先占用一个槽位:

  • 整数表示真实节点,它还需要左右两个孩子的位置,因此消耗一个、新增两个,槽位净增 $1$。
  • # 表示当前位置为空,不再产生孩子,槽位净减 $1$。

按前序顺序,每一项都填入接下来应访问的槽位;真实节点新产生的左右位置,也按先左后右的顺序等待填写。只要还有槽位,下一项就有合法位置可放。因此验证时只需计数,不必保存每个位置属于哪个节点。

合法性需要同时满足两个条件:读取下一项前 slots > 0,否则树已经结束却还有多余内容;全部读完后 slots == 0,否则还有孩子位置没有交代。两者一起保证每一项都能安放,且最终没有遗漏。只有 # 时,根位置恰好被填空,也是一棵合法的空树。

解题步骤

  • 令 slots = 1,从字符串开头逐个 token 扫描。
  • 处理 token 前若 slots == 0,说明前面已经构成完整树,当前 token 无处可放,返回 false。
  • token 为 # 时令 slots--;否则它是数字节点,令 slots++,这已合并了「消耗一个、新增两个」。题目保证格式有效,检查 token 的第一个字符即可分辨这两种情况。
  • 向后扫描到下一个逗号或字符串末尾,整体跳过当前 token;外层循环再越过逗号,定位下一项,不创建分割数组。
  • 所有 token 扫描完后,返回 slots == 0。

代码实现

class Solution {
    public boolean isValidSerialization(String preorder) {
        int slots = 1;
        int n = preorder.length();

        for (int i = 0; i < n; i++) {
            // 读取前必须仍有空位,树已经填满后不能再出现任何节点。
            if (slots == 0) {
                return false;
            }

            // 实节点消耗一个再生成两个,净加一;空标记只消耗一个。
            slots += preorder.charAt(i) == '#' ? -1 : 1;

            // 一次跳过完整数字或空标记,不能把多位数逐字符计成多个节点。
            while (i < n && preorder.charAt(i) != ',') {
                i++;
            }
        }

        return slots == 0;
    }
}
func isValidSerialization(preorder string) bool {
    slots := 1
    for i := 0; i < len(preorder); i++ {
        // 读取前必须仍有空位,树已经填满后不能再出现任何节点。
        if slots == 0 {
            return false
        }

        // 实节点消耗一个再生成两个,净加一;空标记只消耗一个。
        if preorder[i] == '#' {
            slots--
        } else {
            slots++
        }
        // 一次跳过完整数字或空标记,不能把多位数逐字符计成多个节点。
        for i < len(preorder) && preorder[i] != ',' {
            i++
        }
    }
    return slots == 0
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为序列化字符串长度;外层定位 token,内层跳到逗号,每个字符只被常数次访问。
  • 空间复杂度:$O(1)$,只使用槽位计数和扫描下标,没有拆分字符串或建树。

关键点总结

[!green]

  • 槽位表示待填充位置;实节点净增一个,# 净减一个。
  • 判据包含两部分:读 token 前不能无槽位,读完后不能有剩余槽位。
  • 前序顺序保证 token 总是填入下一个槽位,因此一个计数器就能代替建树或栈模拟。
  • 直接扫描到逗号可整体跳过 token,多位整数只计算一次,并把额外空间保持为 $O(1)$。

易错点总结

[!yellow]

  • 只检查整数项与 # 的总数量关系,无法发现前缀已经填满整棵树、后面却还有多余项的情况。
  • 少了结束时的 slots == 0,会接受孩子位置尚未全部填写的残缺序列。
  • 数字节点直接 slots += 2 而忘了它自己也要消耗一个槽位;净变化应是 +1。
  • 按数字字符逐个更新槽位,会把一个多位整数误算成多个节点;必须整体跳过当前 token。
  • 将「槽位更新后为 0」立即判错:最后一个 # 本来就应让槽位归零;错误是归零后还有 token。

相似题目

题目 难度 关联与区别
297. 二叉树的序列化与反序列化 困难 序列化中的空位标记保留结构,本题只检查槽位能否被恰好消费,不实际建树。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/50533951
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!