什么是序列化?

JSON 的运用非常广泛,比如我们经常将变成语言中的结构体序列化成 JSON 字符串,存入缓存或者通过网络发送给远端服务,消费者接受 JSON 字符串然后进行反序列化,就可以得到原始数据了。这就是「序列化」和「反序列化」的目的,以某种固定格式组织字符串,使得数据可以独立于编程语言。
那么假设现在有一棵用 Java 实现的二叉树,我想把它序列化字符串,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行「序列化」和「反序列化」了。

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

我们可以用 serialize 方法将二叉树序列化成字符串,用 deserialize 方法将序列化的字符串反序列化成二叉树,至于以什么格式序列化和反序列化,这个完全由你决定。
比如说输入如下这样一棵二叉树:
image.png
serialize 方法也许会把它序列化成字符串 2,1,#,6,3,#,#,其中 # 表示 null 指针,那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。

想象一下,二叉树结该是一个二维平面内的结构,而序列化出来的字符串是一个线性的一维结构。所谓的序列化不过就是把结构化的数据「打平」,其实就是在考察二叉树的遍历方式。

二叉树的遍历方式有哪些?递归遍历方式有前序遍历,中序遍历,后序遍历;迭代方式一般是层级遍历。本文就把这些方式都尝试一遍,来实现 serialize 方法和 deserialize 方法。

前序遍历

前序遍历,就是在递归遍历左右子树之前写的代码就是前序遍历代码。我们先来看一下伪代码:

  1. LinkedList<Integer> res;
  2. void traverse(TreeNode root) {
  3. if (root == null) {
  4. // 暂且用数字 -1 代表空指针 null
  5. res.addLast(-1);
  6. return;
  7. }
  8. /****** 前序遍历位置 ******/
  9. res.addLast(root.val);
  10. /***********************/
  11. traverse(root.left);
  12. traverse(root.right);
  13. }

调用「traverse」函数之后,将二叉树打平到一个字符串中也是完全一样的:

  1. // 代表分隔符的字符
  2. String SEP = ",";
  3. // 代表 null 空指针的字符
  4. String NULL = "#";
  5. // 用于拼接字符串
  6. StringBuilder sb = new StringBuilder();
  7. // 讲二叉树打平为字符串
  8. void traverse(TreeNode root, StringBuilder sb){
  9. if(root == null){
  10. sb.append(NULL).append(SEP);
  11. return;
  12. }
  13. // 前序遍历
  14. sb.append(root.val).append(SEP);
  15. traverse(root.left, sb);
  16. traverse(root.right, sb);
  17. }

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;

}