一文带你看懂二叉树的序列化

咱们先来看下什么是序列化,如下定义来自维基百科:java

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

可见,序列化和反序列化在计算机科学中的应用仍是很是普遍的。就拿 LeetCode 平台来讲,其容许用户输入形如:node

[1,2,3,null,null,4,5]

这样的数据结构来描述一颗树:git

([1,2,3,null,null,4,5] 对应的二叉树)github

其实序列化和反序列化只是一个概念,不是一种具体的算法,而是不少的算法。而且针对不一样的数据结构,算法也会不同。本文主要讲述的是二叉树的序列化和反序列化。看完本文以后,你就能够放心大胆地去 AC 如下两道题:算法

<!-- more -->数组

前置知识

阅读本文以前,须要你对树的遍历以及 BFS 和 DFS 比较熟悉。若是你还不熟悉,推荐阅读一下相关文章以后再来看。或者我这边也写了一个总结性的文章二叉树的遍历,你也能够看看。网络

前言

咱们知道:二叉树的深度优先遍历,根据访问根节点的顺序不一样,能够将其分为前序遍历中序遍历, 后序遍历。即若是先访问根节点就是前序遍历,最后访问根节点就是后续遍历,其它则是中序遍历。而左右节点的相对顺序是不会变的,必定是先左后右。数据结构

固然也能够设定为先右后左。

而且知道了三种遍历结果中的任意两种便可还原出原有的树结构。这不就是序列化和反序列化么?若是对这个比较陌生的同窗建议看看我以前写的《构造二叉树系列》app

有了这样一个前提以后算法就天然而然了。即先对二叉树进行两次不一样的遍历,不妨假设按照前序和中序进行两次遍历。而后将两次遍历结果序列化,好比将两次遍历结果以逗号“,” join 成一个字符串。 以后将字符串反序列便可,好比将其以逗号“,” split 成一个数组。函数

序列化:

class Solution:
    def preorder(self, root: TreeNode):
        if not root: return []
        return [str(root.val)] +self. preorder(root.left) + self.preorder(root.right)
    def inorder(self, root: TreeNode):
        if not root: return []
        return  self.inorder(root.left) + [str(root.val)] + self.inorder(root.right)
    def serialize(self, root):
        ans = ''
        ans += ','.join(self.preorder(root))
        ans += '$'
        ans += ','.join(self.inorder(root))

        return ans

反序列化:

这里我直接用了力扣 105. 从前序与中序遍历序列构造二叉树 的解法,一行代码都不改。

class Solution:
    def deserialize(self, data: str):
        preorder, inorder = data.split('$')
        if not preorder: return None
        return self.buildTree(preorder.split(','), inorder.split(','))

    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        # 实际上inorder 和 preorder 必定是同时为空的,所以你不管判断哪一个都行
        if not preorder:
            return None
        root = TreeNode(preorder[0])

        i = inorder.index(root.val)
        root.left = self.buildTree(preorder[1:i + 1], inorder[:i])
        root.right = self.buildTree(preorder[i + 1:], inorder[i+1:])

        return root

实际上这个算法是不必定成立的,缘由在于树的节点可能存在重复元素。也就是说我前面说的知道了三种遍历结果中的任意两种便可还原出原有的树结构是不对的,严格来讲应该是若是树中不存在重复的元素,那么知道了三种遍历结果中的任意两种便可还原出原有的树结构

聪明的你应该发现了,上面个人代码用了 i = inorder.index(root.val),若是存在重复元素,那么获得的索引 i 就可能不是准确的。可是,若是题目限定了没有重复元素则能够用这种算法。可是现实中不出现重复元素不太现实,所以须要考虑其余方法。那到底是什么样的方法呢? 接下来进入正题。

DFS

序列化

咱们来模仿一下力扣的记法。 好比:[1,2,3,null,null,4,5](本质上是 BFS 层次遍历),对应的树以下:

选择这种记法,而不是 DFS 的记法的缘由是看起来比较直观

序列化的代码很是简单, 咱们只须要在普通的遍历基础上,增长对空节点的输出便可(普通的遍历是不处理空节点的)。

好比咱们都树进行一次前序遍历的同时增长空节点的处理。选择前序遍历的缘由是容易知道根节点的位置,而且代码好写,不信你能够试试。

所以序列化就仅仅是普通的 DFS 而已,直接给你们看看代码。

Python 代码:

class Codec:
    def serialize_dfs(self, root, ans):
        # 空节点也须要序列化,不然没法惟一肯定一棵树,后不赘述。
        if not root: return ans + '#,'
        # 节点之间经过逗号(,)分割
        ans += str(root.val) + ','
        ans = self.serialize_dfs(root.left, ans)
        ans = self.serialize_dfs(root.right, ans)
        return ans
    def serialize(self, root):
        # 因为最后会添加一个额外的逗号,所以须要去除最后一个字符,后不赘述。
        return self.serialize_dfs(root, '')[:-1]

Java 代码:

public class Codec {
    public String serialize_dfs(TreeNode root, String str) {
        if (root == null) {
            str += "None,";
        } else {
            str += str.valueOf(root.val) + ",";
            str = serialize_dfs(root.left, str);
            str = serialize_dfs(root.right, str);
        }
        return str;
    }

    public String serialize(TreeNode root) {
        return serialize_dfs(root, "");
    }
}

[1,2,3,null,null,4,5] 会被处理为1,2,#,#,3,4,#,#,5,#,#

咱们先看一个短视频:

(动画来自力扣)

反序列化

反序列化的第一步就是将其展开。以上面的例子来讲,则会变成数组:[1,2,#,#,3,4,#,#,5,#,#],而后咱们一样执行一次前序遍历,每次处理一个元素,重建便可。因为咱们采用的前序遍历,所以第一个是根元素,下一个是其左子节点,下下一个是其右子节点。

Python 代码:

def deserialize_dfs(self, nodes):
        if nodes:
            if nodes[0] == '#':
                nodes.pop(0)
                return None
            root = TreeNode(nodes.pop(0))
            root.left = self.deserialize_dfs(nodes)
            root.right = self.deserialize_dfs(nodes)
            return root
        return None

    def deserialize(self, data: str):
        nodes = data.split(',')
        return self.deserialize_dfs(nodes)

Java 代码:

public TreeNode deserialize_dfs(List<String> l) {
        if (l.get(0).equals("None")) {
            l.remove(0);
            return null;
        }

        TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
        l.remove(0);
        root.left = deserialize_dfs(l);
        root.right = deserialize_dfs(l);

        return root;
    }

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

复杂度分析

  • 时间复杂度:每一个节点都会被处理一次,所以时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。
  • 空间复杂度:空间复杂度取决于栈深度,所以空间复杂度为 $O(h)$,其中 $h$ 为树的深度。

BFS

序列化

实际上咱们也可使用 BFS 的方式来表示一棵树。在这一点上其实就和力扣的记法是一致的了。

咱们知道层次遍历的时候其实是有层次的。只不过有的题目须要你记录每个节点的层次信息,有些则不须要。

这其实就是一个朴实无华的 BFS,惟一不一样则是增长了空节点。

Python 代码:

class Codec:
    def serialize(self, root):
        ans = ''
        queue = [root]
        while queue:
            node = queue.pop(0)
            if node:
                ans += str(node.val) + ','
                queue.append(node.left)
                queue.append(node.right)
            else:
                ans += '#,'
        return ans[:-1]

反序列化

如图有这样一棵树:

那么其层次遍历为 [1,2,3,#,#, 4, 5]。咱们根据此层次遍历的结果来看下如何还原二叉树,以下是我画的一个示意图:

容易看出:

  • level x 的节点必定指向 level x + 1 的节点,如何找到 level + 1 呢? 这很容易经过层次遍从来作到。
  • 对于给的的 level x,从左到右依次对应 level x + 1 的节点,即第 1 个节点的左右子节点对应下一层的第 1 个和第 2 个节点,第 2 个节点的左右子节点对应下一层的第 3 个和第 4 个节点。。。
  • 接上,其实若是你仔细观察的话,实际上 level x 和 level x + 1 的判断是无需特别判断的。咱们能够把思路逆转过来:即第 1 个节点的左右子节点对应第 1 个和第 2 个节点,第 2 个节点的左右子节点对应第 3 个和第 4 个节点。。。(注意,没了下一层三个字)

所以咱们的思路也是一样的 BFS,并依次链接左右节点。

Python 代码:

def deserialize(self, data: str):
        if data == '#': return None
        # 数据准备
        nodes = data.split(',')
        if not nodes: return None
        # BFS
        root = TreeNode(nodes[0])
        queue = [root]
        # 已经有 root 了,所以从 1 开始
        i = 1

        while i < len(nodes) - 1:
            node = queue.pop(0)
            #
            lv = nodes[i]
            rv = nodes[i + 1]
            i += 2
            # 对于给的的 level x,从左到右依次对应 level x + 1 的节点
            # node 是 level x 的节点,l 和 r 则是 level x + 1 的节点
            if lv != '#':
                l = TreeNode(lv)
                node.left = l
                queue.append(l)

            if rv != '#':
                r = TreeNode(rv)
                node.right = r
                queue.append(r)
        return root

复杂度分析

  • 时间复杂度:每一个节点都会被处理一次,所以时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。
  • 空间复杂度:$O(N)$,其中 $N$ 为节点的总数。

总结

除了这种方法还有不少方案, 好比括号表示法。 关于这个能够参考力扣606. 根据二叉树建立字符串,这里就再也不赘述了。

本文从 BFS 和 DFS 角度来思考如何序列化和反序列化一棵树。 若是用 BFS 来序列化,那么相应地也须要 BFS 来反序列化。若是用 DFS 来序列化,那么就须要用 DFS 来反序列化。

咱们从马后炮的角度来讲,实际上对于序列化来讲,BFS 和 DFS 都比较常规。对于反序列化,你们能够像我这样举个例子,画一个图。能够先在纸上,电脑上,若是你熟悉了以后,也能够画在脑子里。

(Like This)

更多题解能够访问个人 LeetCode 题解仓库:https://github.com/azl3979858... 。 目前已经 30K star 啦。

关注公众号力扣加加,努力用清晰直白的语言还原解题思路,而且有大量图解,手把手教你识别套路,高效刷题。

相关文章
相关标签/搜索