i007.cc

i007.cc

优先队列-降维打击

05.价值资料

二叉树的序列化与反序列化(Serialize and Deserialize Binary Tree)

力扣第 297 题。把一棵二叉树转成字符串存储,再把字符串还原成原来的树。


先想清楚一个问题:为什么需要记录空节点?

只记录非空节点的前序遍历是 1,2,3,但这棵树和另一棵树可能有相同的序列:

1              1
   / \              \
  2   3              2
                      \
                       3

 

必须把空节点也序列化进去,才能唯一还原一棵树。


最优解法:前序遍历(DFS)⭐

序列化时前序遍历,空节点写 #,节点间用 , 分隔。反序列化时同样按前序顺序消费字符串。

cpp
class Codec {
public:
    // ── 序列化 ──
    string serialize(TreeNode* root) {
        string res;
        dfs_serialize(root, res);
        return res;
    }

    void dfs_serialize(TreeNode* node, string& res) {
        if (!node) {
            res += "#,";
            return;
        }
        res += to_string(node->val) + ",";
        dfs_serialize(node->left,  res);
        dfs_serialize(node->right, res);
    }

    // ── 反序列化 ──
    TreeNode* deserialize(string data) {
        queue<string> q;
        string token;
        for (char c : data) {
            if (c == ',') { q.push(token); token.clear(); }
            else token += c;
        }
        return dfs_deserialize(q);
    }

    TreeNode* dfs_deserialize(queue<string>& q) {
        string val = q.front(); q.pop();
        if (val == "#") return nullptr;

        TreeNode* node = new TreeNode(stoi(val));
        node->left  = dfs_deserialize(q);  // 先建左子树
        node->right = dfs_deserialize(q);  // 再建右子树
        return node;
    }
};

 


为什么前序遍历特别适合这道题?

前序是”根 → 左 → 右”,序列化和反序列化的顺序完全一致——反序列化时每次从队列头取一个值,就知道这是当前子树的根,递归往下建,天然对应。

树:         1
           / \
          2   3
         /
        4

序列化: 1,2,4,#,#,#,3,#,#

反序列化过程:
取 1 → 根节点
  取 2 → 左子树根
    取 4 → 左子树根
      取 # → 4 的左为 null
      取 # → 4 的右为 null
    取 # → 2 的右为 null
  取 3 → 右子树根
    取 # → 3 的左为 null
    取 # → 3 的右为 null
还原完毕 ✓

 


解法二:BFS 层序遍历

cpp
string serialize(TreeNode* root) {
    if (!root) return "";
    string res;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front(); q.pop();
        if (!node) { res += "#,"; continue; }
        res += to_string(node->val) + ",";
        q.push(node->left);
        q.push(node->right);
    }
    return res;
}

TreeNode* deserialize(string data) {
    if (data.empty()) return nullptr;
    // 分割字符串
    vector<string> tokens;
    string token;
    for (char c : data) {
        if (c == ',') { tokens.push_back(token); token.clear(); }
        else token += c;
    }
    int i = 1;
    TreeNode* root = new TreeNode(stoi(tokens[0]));
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front(); q.pop();
        if (tokens[i] != "#") {
            node->left = new TreeNode(stoi(tokens[i]));
            q.push(node->left);
        }
        i++;
        if (tokens[i] != "#") {
            node->right = new TreeNode(stoi(tokens[i]));
            q.push(node->right);
        }
        i++;
    }
    return root;
}

 

BFS 版本更直观(就是层序打印),但代码略长,反序列化需要额外维护索引。


两种解法对比

DFS 前序 BFS 层序
时间 O(n) O(n)
空间 O(h) 递归栈 O(w) 队列(w为最大宽度)
代码简洁度 ★★★★★ ★★★
面试推荐 ✅ 首选 作为第二思路

最坏情况下(满二叉树)BFS 队列宽度 O(n/2),DFS 栈深度 O(log n)——DFS 空间更优。


面试时最容易踩的坑

① 分隔符不能省121,2 是不同的,节点值可能是多位数甚至负数,必须有分隔符。

② 空节点必须序列化:这是还原唯一性的保证,不能只记非空节点。

③ 反序列化用队列而非下标:队列天然支持按序消费,代码更简洁,不容易越界。

把这三点在面试里主动说出来,比直接写代码更能体现理解深度。

发表回复