二叉树的序列化与反序列化

1 阅读3分钟

从二叉树的“最近公共祖先”再进一步,我们来到了一道更具工程意义的题目——力扣297. 二叉树的序列化与反序列化(Serialize and Deserialize Binary Tree)

这是LeetCode上的一道困难(Hard) 题,也是剑指Offer 37的同款题。它在字节跳动、Google、Meta的面试中频繁出现,因为它完美地考察了遍历算法递归/迭代构建树的综合能力。

题目的要求很简单:设计一个算法,能将一棵二叉树序列化成一个字符串,并能将这个字符串反序列化回原来的树结构。

🎯 核心难点:如何用字符串唯一地表达“空”?

最关键的一点是:我们必须显式地记录空节点(null)

如果只记录非空节点的值,比如前序遍历1,2,3,我们无法确定32的左孩子、右孩子,还是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题! 🚀