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

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

前言

    几个月前,笔者参加了一次面试。面试的最后,面试官要求手写“二叉树的序列化与反序列化”。其实我们在掌握了二叉树的算法套路之后,这应该是比较简单的一道题。接下来我们就来看看如何解决它吧!

讲讲序列化

    序列化。一个经常出现的名词,我们都知道Java Bean一般都需要继承Serializable接口,还需要定义一个serialVersionUID。新多人虽这么用过却不知道为什么要这么用。所以为了照顾新人,我们先来讲讲序列化是什么,以及为什么需要序列化。

    首先引用Wikipedia的一段描述

序列化(serialization)在 计算机科学的数据处理中,是指将 数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

    我想维基百科已经解释的非常详细了,那我们想想,为什么需要序列化呢?我们知道序列化可以将一个对象"存起来",那么,我们就可以运行时动态的对象进行网络传输、跨平台存储、转储、RPC等操作。序列化技术可以说是很多技术的祭奠,我们耳熟能详的RPC就是其中一个。

二叉树与序列化

    现在我们已经对序列化有了一个基本的了解。我们不妨思考一下序列化的过程。我们会发现,将一个数组对象序列化很简单,我们可以将 int[] arr = new int[3];序列化成"0,0,0"。将一个对象序列化也很简单,譬如JSON,就是最好的对象描述格式之一。那二叉树该怎么序列化呢?我们怎样把一个二维的“树”转换成一个一围的“串”呢?

    这就需要用到二叉树的遍历,一说到遍历大家就会觉得序列化也没那么难了,因为都知道二叉树的遍历是比较死板的.那么究竟选用前序还是中序,或者后序遍历呢?答案是"随便"。我们只需要保证序列化和反序列化用的是同一种遍历方法即可。那么接下来,我们就借着Leetcode中的297. 二叉树的序列化与反序列化来给大家演示一下序列化与反序列化操作。

    首先还是回顾一下遍历操作,三种不同的遍历方式区别只在于逻辑代码递归代码的相对位置。我们就以前序遍历来演示。

// 二叉树前序遍历套路。
public static void preOrder(TreeNode root){
    if( root == null ) return;
    
    // 前序遍历 逻辑代码
    doSomething();
    
    preOrder(root.left);
    preOrder(root.right);
}

    基于这个骨架,我们来看看二叉树的序列化代码应该怎么写。按照我们刚才的框架,我们首先应该想到的代码是这样的。

class Codec {
    final String NULL = "null"; // 用'null'代替表示空节点;
    final String SEP = ",";
    StringBuilder res = new StringBuilder();
    
    public void serialize(TreeNode root) {
        if ( root == null ) {
            res.append(NULL);
            res.append(SEP);
        }
        else {
            res.append(String.valueOf(root.val));
            res.append(SEP);
            serialize(root.left);
            serialize(root.right);
        }
    }
}

    但是我们会发现,明明一个简单的递归,写出来却很复杂。而且力扣297给的方法签名是这个样子的。String serialize(TreeNode root)。那这样一变,可能萌新们变得无从下手,不会套框架了。我们来看看怎样在递归中正确的返回我们想要的结果。

public String serialize(TreeNode root) {
        if (root == null) return NULL;
        return String.valueOf(root.val) + SEP + serialize(root.left) + SEP + serialize(root.right);
    }

    至此,我们得到的字符串类似于 "1,2,null,null,3,null,null"

    相对序列化来说,反序列化稍微麻烦一点,因为方法需要返回一个TreeNode节点,所以我们在一个方法中是无法完成递归添加元素并返回的。这时候我们就需要一个辅助方法来独立地完成递归,在另一个方法中返回。

TreeNode helper(List strs) {
        if (strs.get(0).equals(NULL)) {
            strs.remove(0);
            return null;
        }
        TreeNode root = new TreeNode(Integer.parseInt(strs.get(0)));
        strs.remove(0);
        root.left = helper(strs);
        root.right = helper(strs);
        return root;
    }

    public TreeNode deserialize(String data) {
        String[] data_array = data.split(",");
        List data_list = new ArrayList<>(Arrays.asList(data_array));
        return helper(data_list);
    }

    这么一看,二叉树的序列化与反序列化是不是很简单呢?但是其实这里面有一个非常重要的前提条件,那就是"null"。有的题目会给一个字符串,但是其中的空元素并没有用null标识,或者有其他的题目变种,一般来说,我们都很难从字符串直接构造出二叉树结构。需要通过两种不同的遍历方式确定root节点的位置。但总体来说难度不算很大,这里给大家留一个作业,看看自己是否可以举一反三,对Leetcode此类题做一个集中训练,将变种题目也收入囊中。

    Bye ~

你可能感兴趣的:(算法-数据结构,java)