算法-二叉树的序列化与反序列化

一、什么是二叉树的序列化与反序列化?

简单来说:

  • 序列化:把内存中的二叉树,转成字符串(或数组)的形式,方便存储(如存文件)或网络传输。
  • 反序列化:把序列化得到的字符串,还原回原来的二叉树结构,恢复成可操作的内存数据。

举个直观例子:

一棵简单二叉树

复制代码
    1
   / \
  2   3

序列化后可能变成字符串 1!2!##3!##,反序列化则是把 1!2!##3!## 再变回上面的树。

二、核心规则约定

要实现序列化和反序列化,首先得定好 "编码规则"(双方都要遵守,否则无法还原),本文约定:

  1. 空节点标记 :用 # 表示空节点(如叶子节点的左右子树都是 #);
  2. 节点值分隔符 :用 ! 分隔不同节点的值(避免多位数混淆,如 12!3 明确是节点 12 和 3,而非 1、2、3);
  3. 遍历顺序:用「前序遍历」(根→左→右),因为前序遍历先访问根节点,反序列化时能快速定位根,再构建左右子树。

三、序列化实现(树→字符串)

3.1 完整代码

复制代码
#include <iostream>
#include <string>
using namespace std;

// 二叉树节点定义(LeetCode环境自带,本地调试需补充)
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

class Solution {
public:
    // 辅助递归函数:拼接节点信息到字符串
    void SerializeFun(TreeNode *root, string &res) {
        // 1. 处理空节点:拼 #
        if (root == NULL) {
            res += '#';
            return;
        }
        // 2. 处理非空节点:拼 节点值 + !(!作为分隔符)
        string tmp = to_string(root->val);
        res += tmp + '!';
        // 3. 前序遍历:递归处理左、右子树
        SerializeFun(root->left, res);
        SerializeFun(root->right, res);
    }

    // 主函数:返回C风格字符串(兼容C语言场景)
    char* Serialize(TreeNode *root) {    
        // 特殊情况:空树直接返回 "#"
        if (root == NULL) return "#";
        
        string res;          // 用C++ string拼接(方便操作)
        SerializeFun(root, res);  // 调用递归生成字符串
        
        // 把C++ string转成C风格char*(需手动管理内存)
        char *charRes = new char[res.length() + 1];  // +1是留位置存字符串结束符'\0'
        strcpy(charRes, res.c_str());  // 复制内容(c_str()把string转成C风格字符串)
        charRes[res.length()] = '\0';  // 手动加结束符(C风格字符串必须以'\0'结尾)
        
        return charRes;
    }
};

3.2 代码逻辑拆解(结合例子)

以树 1→2(左)、3(右) 为例,看序列化过程:

步骤 1:处理根节点 1
  • root=1 非空,转成字符串 "1",拼上 !res = "1!"
  • 递归处理左子树(节点 2),再处理右子树(节点 3)。
步骤 2:处理左子树节点 2
  • root=2 非空,拼 "2!"res = "1!2!"
  • 递归处理 2 的左子树(空节点):拼 #res = "1!2!#"
  • 递归处理 2 的右子树(空节点):拼 #res = "1!2!##"
步骤 3:处理右子树节点 3
  • root=3 非空,拼 "3!"res = "1!2!##3!"
  • 递归处理 3 的左子树(空节点):拼 #res = "1!2!##3!#"
  • 递归处理 3 的右子树(空节点):拼 #res = "1!2!##3!##"
步骤 4:转成 C 风格字符串
  • 最终 res"1!2!##3!##",通过 c_str() 转成 C 风格字符串,再用 strcpy 复制到 charRes 中,返回 charRes

3.3 关键知识点:c_str () 与 strcpy ()

  • c_str() :C++ string 的成员函数,作用是把 string 转成 C 风格字符串 (即 const char* 类型,以 '\0' 结尾)。比如 res.c_str() 会返回指向 "1!2!##3!##\0" 的指针。
  • strcpy(dst, src) :C 语言函数,把 src 指向的 C 风格字符串,复制到 dst 指向的字符数组中。注意 dst 的空间必须足够大,否则会内存溢出。

四、反序列化实现(字符串→树)

4.1 完整代码

复制代码
#include <iostream>
#include <cstring>  // 包含strcpy函数
using namespace std;

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

class Solution {
public:
    // 辅助递归函数:解析字符串,构建二叉树
    // 参数用char**:因为要修改外层字符串指针的位置(同步读取进度)
    TreeNode* DeserializeFunction(char** str) {
        // 1. 处理空节点:遇到 # 说明是空节点
        if (**str == '#') {
            (*str)++;  // 指针后移,跳过 #(下次读下一个字符)
            return NULL;
        }

        // 2. 解析非空节点的值(处理多位数,如 "12"→12)
        int val = 0;
        // 循环读字符,直到遇到 !(值的分隔符)或 \0(字符串结束)
        while (**str != '!' && **str != '\0') {
            val = val * 10 + ((**str) - '0');  // 字符转数字:'1'-'0'=1
            (*str)++;  // 指针后移
        }

        // 3. 创建当前节点
        TreeNode* root = new TreeNode(val);

        // 4. 跳过值的分隔符 !(如果没到字符串末尾)
        if (**str == '\0') {
            return root;  // 字符串结束,没有子树了
        } else {
            (*str)++;  // 跳过 !
        }

        // 5. 前序遍历:递归构建左、右子树(和序列化顺序一致)
        root->left = DeserializeFunction(str);
        root->right = DeserializeFunction(str);

        return root;
    }

    // 主函数:入口,处理空树场景
    TreeNode* Deserialize(char *str) {
        // 特殊情况:字符串是 "#",说明是空树
        if (strcmp(str, "#") == 0) {  // 用strcmp比较字符串(避免直接==比较指针)
            return NULL;
        }
        // 传str的地址(char**类型),让递归函数能修改str的位置
        return DeserializeFunction(&str);
    }
};

4.2 代码逻辑拆解(结合例子)

以字符串 1!2!##3!## 为例,看如何还原成原树:

步骤 1:主函数处理
  • 字符串不是 "#",调用 DeserializeFunction(&str),传 str 的地址(str 初始指向字符串第一个字符 '1')。
步骤 2:解析根节点 1
  1. **str'1'(非 #),进入值解析;
  2. 循环读 '1'val = 0*10 +1=1,指针后移到 '!'
  3. 创建节点 1,跳过 '!'(指针后移到 '2');
  4. 递归构建左子树(解析 '2'),再构建右子树(解析 '3')。
步骤 3:解析左子树节点 2
  1. **str'2',解析值为 2,创建节点 2;
  2. 跳过 '!',指针后移到 '#'
  3. 递归构建 2 的左子树:遇到 '#',返回 NULL,指针后移到下一个 '#'
  4. 递归构建 2 的右子树:遇到 '#',返回 NULL,指针后移到 '3'
  5. 节点 2 的左右都是 NULL,返回节点 2(作为 1 的左子树)。
步骤 4:解析右子树节点 3
  1. **str'3',解析值为 3,创建节点 3;
  2. 跳过 '!',指针后移到 '#'
  3. 递归构建 3 的左子树:遇到 '#',返回 NULL,指针后移到下一个 '#'
  4. 递归构建 3 的右子树:遇到 '#',返回 NULL,指针后移到 '\0'
  5. 节点 3 的左右都是 NULL,返回节点 3(作为 1 的右子树)。
步骤 5:完成构建
  • 根节点 1 的左右子树都构建完成,返回根节点 1,反序列化结束。

4.3 关键知识点:为什么用 char** str?

如果用 char* str(单指针):

  • 递归函数内部修改 str(如 str++),只会修改函数内部的副本,外层的 str 不会变;
  • 导致每次递归都从字符串开头读,无法正确推进读取进度。

char** str(双指针):

  • 可以直接修改外层 str 的指向(通过 (*str)++),保证递归过程中,字符串的读取位置是 "连续推进" 的(从 '1''2',再到 '#' 等)。

五、测试验证

5.1 测试流程

  1. 构建一棵二叉树;
  2. 调用 Serialize 转成字符串;
  3. 调用 Deserialize 把字符串还原成树;
  4. 验证还原后的树和原树结构一致。

5.2 代码示例

复制代码
int main() {
    // 构建原树:1→2(左)、3(右)
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);

    Solution sol;
    // 序列化
    char* serializedStr = sol.Serialize(root);
    cout << "序列化结果:" << serializedStr << endl;  // 输出:1!2!##3!##

    // 反序列化
    TreeNode* deserializedRoot = sol.Deserialize(serializedStr);
    cout << "反序列化后根节点值:" << deserializedRoot->val << endl;  // 输出:1
    cout << "反序列化后左子树值:" << deserializedRoot->left->val << endl;  // 输出:2
    cout << "反序列化后右子树值:" << deserializedRoot->right->val << endl;  // 输出:3

    // 释放内存(避免内存泄漏)
    delete deserializedRoot->left;
    delete deserializedRoot->right;
    delete deserializedRoot;
    delete[] serializedStr;

    return 0;
}
相关推荐
蒹葭玉树4 小时前
【C++上岸】C++常见面试题目--算法篇(第二十期)
c++·算法·面试
JJJJ_iii4 小时前
【左程云算法03】对数器&算法和数据结构大致分类
数据结构·算法·分类
轮到我狗叫了4 小时前
牛客.小红的子串牛客.kotori和抽卡牛客.循环汉诺塔牛客.ruby和薯条
java·开发语言·算法
高山有多高4 小时前
详解文件操作
c语言·开发语言·数据库·c++·算法
乌萨奇也要立志学C++4 小时前
【洛谷】队列相关经典算法题详解:模板队列、机器翻译、海港
算法
YuTaoShao5 小时前
【LeetCode 热题 100】49. 字母异位词分组
算法·leetcode·哈希算法
aliedudu6 小时前
决策树概念与原理
算法·决策树·机器学习
程序员Xu7 小时前
【LeetCode热题100道笔记】腐烂的橘子
笔记·算法·leetcode
天选之女wow7 小时前
【代码随想录算法训练营——Day4】链表——24.两两交换链表中的节点、19.删除链表的倒数第N个节点、面试题02.07.链表相交、142.环形链表II
数据结构·算法·leetcode·链表