从二叉树的“最近公共祖先”再进一步,我们来到了一道更具工程意义的题目——力扣297. 二叉树的序列化与反序列化(Serialize and Deserialize Binary Tree)。
这是LeetCode上的一道困难(Hard) 题,也是剑指Offer 37的同款题。它在字节跳动、Google、Meta的面试中频繁出现,因为它完美地考察了遍历算法和递归/迭代构建树的综合能力。
题目的要求很简单:设计一个算法,能将一棵二叉树序列化成一个字符串,并能将这个字符串反序列化回原来的树结构。
🎯 核心难点:如何用字符串唯一地表达“空”?
最关键的一点是:我们必须显式地记录空节点(null)。
如果只记录非空节点的值,比如前序遍历1,2,3,我们无法确定3是2的左孩子、右孩子,还是1的右孩子。只有把空节点也记录下来,才能完整保留树的结构信息。
💎 解法一:深度优先搜索(DFS)——前序遍历
这是最直观、最推荐的解法。序列化时采用根 -> 左 -> 右的顺序,遇到null用特殊字符(如"N")表示。
- 序列化:前序遍历,遇到空节点追加
"N",否则追加节点值。 - 反序列化:将字符串按分隔符拆成列表,用一个全局索引顺序读取。读到
"N"返回null,否则创建节点,并递归构建其左子树和右子树。
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serializeHelper(root, sb);
return sb.toString();
}
private void serializeHelper(TreeNode node, StringBuilder sb) {
if (node == null) {
sb.append("N,");
return;
}
sb.append(node.val).append(",");
serializeHelper(node.left, sb);
serializeHelper(node.right, sb);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
Queue<String> nodes = new LinkedList<>(Arrays.asList(data.split(",")));
return deserializeHelper(nodes);
}
private TreeNode deserializeHelper(Queue<String> nodes) {
String val = nodes.poll();
if (val.equals("N")) return null;
TreeNode node = new TreeNode(Integer.parseInt(val));
node.left = deserializeHelper(nodes);
node.right = deserializeHelper(nodes);
return node;
}
}
注意:这里使用了
Queue来顺序消费节点值,可以避免维护全局索引的麻烦。
🚀 解法二:广度优先搜索(BFS)——层序遍历
BFS是另一种经典解法。序列化时按层遍历,遇到null记录"N";反序列化时同样使用队列构建。
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) return "N,";
StringBuilder sb = new StringBuilder();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node == null) {
sb.append("N,");
} else {
sb.append(node.val).append(",");
q.offer(node.left);
q.offer(node.right);
}
}
return sb.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] vals = data.split(",");
if (vals[0].equals("N")) return null;
Queue<TreeNode> q = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
q.offer(root);
int i = 1;
while (!q.isEmpty()) {
TreeNode parent = q.poll();
// 处理左孩子
if (!vals[i].equals("N")) {
TreeNode left = new TreeNode(Integer.parseInt(vals[i]));
parent.left = left;
q.offer(left);
}
i++;
// 处理右孩子
if (!vals[i].equals("N")) {
TreeNode right = new TreeNode(Integer.parseInt(vals[i]));
parent.right = right;
q.offer(right);
}
i++;
}
return root;
}
}
BFS的序列化结果更接近LeetCode的官方格式,且反序列化是迭代的,无递归栈溢出风险。
📊 两种解法对比
| 解法 | 序列化顺序 | 反序列化方式 | 优点 | 缺点 |
|---|---|---|---|---|
| DFS (前序) | 根 -> 左 -> 右 | 递归构建 | 代码简洁直观 | 树很深时有栈溢出风险 |
| BFS (层序) | 从上到下,从左到右 | 迭代构建 | 无反序列化栈溢出风险 | 序列化字符串可能包含更多尾部的"N" |
两种解法的时间复杂度都是O(n),空间复杂度也都是O(n)。
💎 总结
这道题的核心在于用特殊符号显式地记录空节点,以唯一确定树的结构。
- DFS(前序遍历) 代码更简洁,是很多人的首选。
- BFS(层序遍历) 更符合LeetCode的官方格式,且迭代构建更稳健。
在实际面试中,可以根据自己的偏好选择一种,并清晰地解释其工作原理。
如果你觉得这篇题解帮你彻底搞懂了二叉树的序列化与反序列化,欢迎点赞、收藏、转发!我们下一站将挑战 力扣124「二叉树中的最大路径和」 ——一道真正考验后序遍历理解的Hard题! 🚀