LeetCode 剑指 Offer 37. 序列化二叉树
题目描述




题意分析
将二叉树编码成字符串,再从该字符串恢复节点值和左右孩子关系都相同的树。值可以重复,因此不能仅靠数值判断结构;编码格式可自行约定,但序列化和反序列化必须配套。
解法:带空位标记的层序编码
核心思路
[!blue]
使用层序遍历,按从上到下、同层从左到右的顺序输出。真实节点写入数值,并把左右孩子都入队;空节点写入
#,但不再为它扩展孩子。这样左右空位也被保留,节点值相同或某侧缺失都不会造成歧义。用逗号分隔每个值和空标记,使多位数、负数仍能作为一个完整 token 读取。空树单独约定:当前 Java 实现返回
null,Go 实现返回空串,各自的反序列化函数先识别同一约定。还原非空树时,先读取根值,并将根放入待填孩子的队列。每取出一个真实父节点,就依次消费左、右两个 token:遇到数值则创建对应孩子并入队,遇到
#则保持该孩子为空。无论是否为空,两个槽位都必须各消费一次。两边真实父节点的处理顺序相同,每个父节点的左右位置又都有明确标记,因此可以逐层唯一恢复原树。序列化队列允许空节点,反序列化队列只放真实节点;两者职责不同,不能照搬空节点入队规则。后者队列为空时,所有真实节点的孩子槽位都已处理完。
解题步骤
- 空树按各语言约定编码:Java 为 null,Go 为空串。
- 序列化时 BFS,空孩子也入队并输出 #,空节点不继续扩展。
- 以逗号分隔 token;反序列化先建根,再逐个填充队首节点的两个孩子。
- 每个孩子槽位都推进下标,只有非空孩子才入队。
代码实现
class Codec {
public String serialize(TreeNode root) {
if (root == null) {
return null;
}
List<String> answer = new ArrayList<>();
Deque<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node != null) {
answer.add(node.val + "");
q.offer(node.left);
q.offer(node.right);
} else {
answer.add("#");
}
}
return String.join(",", answer);
}
public TreeNode deserialize(String data) {
if (data == null) {
return null;
}
String[] vals = data.split(",");
int i = 0;
TreeNode root = new TreeNode(Integer.valueOf(vals[i++]));
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (!"#".equals(vals[i])) {
node.left = new TreeNode(Integer.valueOf(vals[i]));
q.offer(node.left);
}
++i;
if (!"#".equals(vals[i])) {
node.right = new TreeNode(Integer.valueOf(vals[i]));
q.offer(node.right);
}
++i;
}
return root;
}
}
import (
"strconv"
"strings"
)
type Codec struct {
}
func Constructor() Codec {
return Codec{}
}
func (this *Codec) serialize(root *TreeNode) string {
if root == nil {
return ""
}
q := []*TreeNode{
root,
}
answer := []string{}
for len(q) > 0 {
node := q[0]
q = q[1:]
if node != nil {
answer = append(answer, strconv.Itoa(node.Val))
q = append(q, node.Left)
q = append(q, node.Right)
} else {
answer = append(answer, "#")
}
}
return strings.Join(answer, ",")
}
func (this *Codec) deserialize(data string) *TreeNode {
if data == "" {
return nil
}
vals := strings.Split(data, ",")
v, _ := strconv.Atoi(vals[0])
i := 1
root := &TreeNode{Val: v}
q := []*TreeNode{
root,
}
for len(q) > 0 {
node := q[0]
q = q[1:]
if x, err := strconv.Atoi(vals[i]); err == nil {
node.Left = &TreeNode{Val: x}
q = append(q, node.Left)
}
i++
if x, err := strconv.Atoi(vals[i]); err == nil {
node.Right = &TreeNode{Val: x}
q = append(q, node.Right)
}
i++
}
return root
}
复杂度分析
- 时间复杂度:$O(n)$,
n为真实节点数,固定宽度整数的编码、解析均为常数时间。非空树有n个真实节点和n + 1个空孩子标记,共2n + 1个 token。- 空间复杂度:$O(n)$,保存编码结果、拆分后的 token 和层序队列。
关键点总结
[!green]
反序列化输入按题意来自同一序列化器;左右槽位的消费顺序必须与编码顺序一致。
易错点总结
[!yellow]
- Java 序列化队列含 null,使用支持空元素的 LinkedList,不能直接换成 ArrayDeque。
- 只写非空值会丢失左右孩子位置,无法唯一还原结构。
- 遇到 # 也必须移动 token 下标,且不能把 # 继续扩展成孩子。
- 空树约定必须配对;值分隔符不能与负号混淆。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 449. 序列化和反序列化二叉搜索树 | 中等 | BST 的有序性质允许更紧凑的编码,普通二叉树不能依赖该性质省去结构信息。 |
| 331. 验证二叉树的前序序列化 | 中等 | 同样用空位标记描述结构,原题验证前序序列是否合法,本题按层序实际还原节点。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!