什么是序列化?
JSON 的运用非常广泛,比如我们经常将变成语言中的结构体序列化成 JSON 字符串,存入缓存或者通过网络发送给远端服务,消费者接受 JSON 字符串然后进行反序列化,就可以得到原始数据了。这就是「序列化」和「反序列化」的目的,以某种固定格式组织字符串,使得数据可以独立于编程语言。
那么假设现在有一棵用 Java 实现的二叉树,我想把它序列化字符串,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行「序列化」和「反序列化」了。
297. 二叉树的序列化与反序列化
我们可以用 serialize 方法将二叉树序列化成字符串,用 deserialize 方法将序列化的字符串反序列化成二叉树,至于以什么格式序列化和反序列化,这个完全由你决定。
比如说输入如下这样一棵二叉树:
serialize 方法也许会把它序列化成字符串 2,1,#,6,3,#,#,其中 # 表示 null 指针,那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。
想象一下,二叉树结该是一个二维平面内的结构,而序列化出来的字符串是一个线性的一维结构。所谓的序列化不过就是把结构化的数据「打平」,其实就是在考察二叉树的遍历方式。
二叉树的遍历方式有哪些?递归遍历方式有前序遍历,中序遍历,后序遍历;迭代方式一般是层级遍历。本文就把这些方式都尝试一遍,来实现 serialize 方法和 deserialize 方法。
前序遍历
前序遍历,就是在递归遍历左右子树之前写的代码就是前序遍历代码。我们先来看一下伪代码:
LinkedList<Integer> res;void traverse(TreeNode root) {if (root == null) {// 暂且用数字 -1 代表空指针 nullres.addLast(-1);return;}/****** 前序遍历位置 ******/res.addLast(root.val);/***********************/traverse(root.left);traverse(root.right);}
调用「traverse」函数之后,将二叉树打平到一个字符串中也是完全一样的:
// 代表分隔符的字符String SEP = ",";// 代表 null 空指针的字符String NULL = "#";// 用于拼接字符串StringBuilder sb = new StringBuilder();// 讲二叉树打平为字符串void traverse(TreeNode root, StringBuilder sb){if(root == null){sb.append(NULL).append(SEP);return;}// 前序遍历sb.append(root.val).append(SEP);traverse(root.left, sb);traverse(root.right, sb);}
StringBuilder 可以用于高效拼接字符串,所以也可以认为是一个列表,用 , 作为分隔符,用 # 表示空指针 null,调用完 traverse 函数后,StringBuilder 中的字符串应该是 1,2,#,4,#,#,3,#,#,。
现在,我们来写「deserialize」函数,也就是将字符串反过来构造二叉树。
首先我们可以把字符串转化成列表:
String data = "1,2,#,4,#,#,3,#,#,";
String[] nodes = data.split(",");
这样一来,nodes 列表就是二叉树的前序遍历的结果,现在问题是:如何通过二叉树的前序遍历结果还原二叉树。
PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树。
那么,反序列化过程也是一样,先确定根节点 root,然后遵循前序遍历的规则,递归生成左右子树即可:
// 代表分隔符的字符
String SEP = ",";
// 代表 null 空指针的字符
String NULL = "#";
// 用于拼接字符串
StringBuilder sb = new StringBuilder();
/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb){
if(root == null){
sb.append(NULL).append(SEP);
return;
}
// 前序遍历
sb.append(root.val).append(SEP);
serialize(root.left, sb);
serialize(root.right, sb);
}
/* 主函数,将二叉树序列化为字符串 */
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serialize(root, sb);
return sb.toString();
}
/* 主函数,将字符串反序列化为二叉树结构 */
public TreeNode deserialize(String data) {
// 将字符串转化为列表
LinkedList<String> nodes = new LinkedList<>();
for(String s : data.split(SEP)){
nodes.add(s);
}
return deserialize(nodes);
}
/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<String> nodes){
if(nodes.isEmpty()) return null;
// 前序遍历
// 列表的最左段就是根节点,每次移除第一个元素,因为是前序遍历结果
String first = nodes.removeFirst();
if(first.equals(NULL)) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
root.left = deserialize(nodes);
root.right = deserialize(nodes);
return root;
}
