一、什么是二叉树的序列化与反序列化?
简单来说:
- 序列化:把内存中的二叉树,转成字符串(或数组)的形式,方便存储(如存文件)或网络传输。
- 反序列化:把序列化得到的字符串,还原回原来的二叉树结构,恢复成可操作的内存数据。
举个直观例子:
一棵简单二叉树
1
/ \
2 3
序列化后可能变成字符串 1!2!##3!##
,反序列化则是把 1!2!##3!##
再变回上面的树。
二、核心规则约定
要实现序列化和反序列化,首先得定好 "编码规则"(双方都要遵守,否则无法还原),本文约定:
- 空节点标记 :用
#
表示空节点(如叶子节点的左右子树都是#
); - 节点值分隔符 :用
!
分隔不同节点的值(避免多位数混淆,如12!3
明确是节点 12 和 3,而非 1、2、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
**str
是'1'
(非#
),进入值解析;- 循环读
'1'
:val = 0*10 +1=1
,指针后移到'!'
; - 创建节点 1,跳过
'!'
(指针后移到'2'
); - 递归构建左子树(解析
'2'
),再构建右子树(解析'3'
)。
步骤 3:解析左子树节点 2
**str
是'2'
,解析值为 2,创建节点 2;- 跳过
'!'
,指针后移到'#'
; - 递归构建 2 的左子树:遇到
'#'
,返回 NULL,指针后移到下一个'#'
; - 递归构建 2 的右子树:遇到
'#'
,返回 NULL,指针后移到'3'
; - 节点 2 的左右都是 NULL,返回节点 2(作为 1 的左子树)。
步骤 4:解析右子树节点 3
**str
是'3'
,解析值为 3,创建节点 3;- 跳过
'!'
,指针后移到'#'
; - 递归构建 3 的左子树:遇到
'#'
,返回 NULL,指针后移到下一个'#'
; - 递归构建 3 的右子树:遇到
'#'
,返回 NULL,指针后移到'\0'
; - 节点 3 的左右都是 NULL,返回节点 3(作为 1 的右子树)。
步骤 5:完成构建
- 根节点 1 的左右子树都构建完成,返回根节点 1,反序列化结束。
4.3 关键知识点:为什么用 char** str?
如果用 char* str
(单指针):
- 递归函数内部修改
str
(如str++
),只会修改函数内部的副本,外层的str
不会变; - 导致每次递归都从字符串开头读,无法正确推进读取进度。
用 char** str
(双指针):
- 可以直接修改外层
str
的指向(通过(*str)++
),保证递归过程中,字符串的读取位置是 "连续推进" 的(从'1'
到'2'
,再到'#'
等)。
五、测试验证
5.1 测试流程
- 构建一棵二叉树;
- 调用
Serialize
转成字符串; - 调用
Deserialize
把字符串还原成树; - 验证还原后的树和原树结构一致。
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;
}