【算法思想·二叉树】序列化

本文参考labuladong算法笔记[二叉树心法(序列化篇) | labuladong 的算法笔记]

要说序列化和反序列化,得先从 JSON 数据格式说起。

JSON 的运用非常广泛,比如我们经常将编程语言中的结构体序列化成 JSON 字符串,存入缓存或者通过网络发送给远端服务,消费者接受 JSON 字符串然后进行反序列化,就可以得到原始数据了。

这就是序列化和反序列化的目的,以某种特定格式组织数据,使得数据可以独立于编程语言。

那么假设现在有一棵用 Java 实现的二叉树,我想把它通过某些方式存储下来,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行序列化和反序列化了。

概述:前/中/后序和二叉树的唯一性

谈具体的题目之前,我们先思考一个问题:什么样的序列化的数据可以反序列化出唯一的一棵二叉树

比如说,如果给你一棵二叉树的前序遍历结果,你是否能够根据这个结果还原出这棵二叉树呢?

答案是也许可以,也许不可以,具体要看你给的前序遍历结果是否包含空指针的信息。如果包含了空指针,那么就可以唯一确定一棵二叉树,否则就不行。

举例来说,如果我给你这样一个不包含空指针的前序遍历结果 [1,2,3,4,5],那么如下两棵二叉树都是满足这个前序遍历结果的:

所以给定不包含空指针信息的前序遍历结果,是不能还原出唯一的一棵二叉树的。

正确答案是,即便你包含了空指针的信息,也只有前序和后序的遍历结果才能唯一还原二叉树,中序遍历结果做不到。

更直观的,比如如下两棵二叉树显然拥有不同的结构,但它俩的中序遍历结果都是 [#,1,#,1,#],无法区分:

核心区别

总结下结论,当二叉树中节点的值不存在重复时

  1. 如果你的序列化结果中不包含空指针的信息,且你只给出一种遍历顺序,那么你无法还原出唯一的一棵二叉树。

  2. 如果你的序列化结果中不包含空指针的信息,且你会给出两种遍历顺序,那么按照前文 东哥手把手带你刷二叉树(构造篇) 所说,分两种情况:

    2.1. 如果你给出的是前序和中序,或者后序和中序,那么你可以还原出唯一的一棵二叉树。

    2.2. 如果你给出前序和后序,那么你无法还原出唯一的一棵二叉树。

  3. 如果你的序列化结果中包含空指针的信息,且你只给出一种遍历顺序,也要分两种情况:

    3.1. 如果你给出的是前序或者后序,那么你可以还原出唯一的一棵二叉树。

    3.2. 如果你给出的是中序,那么你无法还原出唯一的一棵二叉树。

297 「二叉树的序列化与反序列化」

class Codec:
    
    # 把一棵二叉树序列化成字符串
    def serialize(self, root: TreeNode) -> str:
        pass

    # 把字符串反序列化成二叉树
    def deserialize(self, data: str) -> TreeNode:
        pass

比如说输入如下这样一棵二叉树:

serialize 方法也许会把它序列化成字符串 2,1,#,6,#,#,3,#,#,其中 # 表示 null 指针,那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。

也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。

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

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

1、前序遍历解法

res = []

def traverse(root):
    if root is None:
        # 暂且用数字 -1 代表空指针 null
        res.append(-1)
        return

    # ****** 前序位置 ******
    res.append(root.val)
    # **********************

    traverse(root.left)
    traverse(root.right)

调用 traverse 函数之后,你是否可以立即想出这个 res 列表中元素的顺序是怎样的?比如如下二叉树(# 代表空指针 null),可以直观看出前序遍历做的事情:

那么 res = [1,2,-1,4,-1,-1,3,-1,-1],这就是将二叉树「打平」到了一个列表中,其中 -1 代表 null。

那么,将二叉树打平到一个字符串中也是完全一样的:

# 代表分隔符的字符
SEP = ","
# 代表 null 空指针的字符
NULL = "#"

# 用于拼接字符串
sb = []

# 将二叉树打平为字符串
def traverse(root, sb):
    if root == None:
        sb.append(NULL).append(SEP)
        return

    # ***** 前序位置 *****
    sb.append(str(root.val)).append(SEP)
    # *******************

    traverse(root.left, sb)
    traverse(root.right, sb)

用 , 作为分隔符,用 # 表示空指针 null,调用完 traverse 函数后,sb 中的字符串应该是 1,2,#,4,#,#,3,#,#,

serialize 

class Codec:
    SEP = ","
    NULL = "#"

    # 主函数,将二叉树序列化为字符串
    def serialize(self, root):
        sb = []
        self._serialize(root, sb)
        return "".join(sb)

    # 辅助函数,将二叉树存入 StringBuilder
    def _serialize(self, root, sb):
        if root is None:
            sb.append(self.NULL)
            sb.append(self.SEP)
            return

        # ****** 前序位置 ********
        sb.append(str(root.val))
        sb.append(self.SEP)
        # ***********************

        self._serialize(root.left, sb)
        self._serialize(root.right, sb)

现在,思考一下如何写 deserialize 函数,将字符串反过来构造二叉树。

首先我们可以把字符串转化成列表:

data = "1,2,#,4,#,#,3,#,#,"
nodes = data.split(",")

提示

前文 东哥带你刷二叉树(构造篇) 说过,至少要得到前、中、后序遍历中的两种互相配合才能还原二叉树。那是因为前文的遍历结果没有记录空指针的信息。这里的 nodes 列表包含了空指针的信息,所以只使用 nodes 列表就可以还原二叉树。

根据我们刚才的分析,nodes 列表就是一棵打平的二叉树:

那么,反序列化过程也是一样,先确定根节点 root,然后遵循前序遍历的规则,递归生成左右子树即可

【deserialize】

class Codec:
    SEP = ","
    NULL = "#"

    # 主函数,将字符串反序列化为二叉树结构
    def deserialize(self, data: str) -> TreeNode:
        # 将字符串转化成列表
        nodes = data.split(self.SEP)
        return self._deserialize(nodes)

    # 辅助函数,通过 nodes 列表构造二叉树
    def _deserialize(self, nodes: List[str]) -> TreeNode:
        if not nodes: return None

        # ****** 前序位置 *******
        # 列表最左侧就是根节点
        first = nodes.pop(0)
        if first == self.NULL: return None
        root = TreeNode(int(first)) 
        # *********************

        root.left = self._deserialize(nodes)
        root.right = self._deserialize(nodes)

        return root

我们发现,根据树的递归性质,nodes 列表的第一个元素就是一棵树的根节点,所以只要将列表的第一个元素取出作为根节点,剩下的交给递归函数去解决即可。

2、后序遍历解法

明白了前序遍历的解法,后序遍历就比较容易理解了。serialize 序列化方法非常容易实现,只需要稍微修改前文的 _serialize辅助方法即可:

# 辅助函数,将二叉树存入 StringBuilder
def _serialize(root, sb):
    if root is None:
        sb.append(NULL).append(SEP)
        return

    _serialize(root.left, sb)
    _serialize(root.right, sb)
    
    # ****** 后序位置 ********
    sb.append(root.val).append(SEP)
    # ***********************

后序遍历导致结果的顺序发生变化:

关键点在于,如何实现后序遍历的 deserialize 方法呢?

deserialize 方法首先寻找 root 节点的值,然后递归计算左右子节点。那么我们这里也应该顺着这个基本思路走,后序遍历中,root 节点的值能不能找到?

注意,根据上图,从后往前在 nodes 列表中取元素,一定要先构造 root.right 子树,后构造 root.left 子树

【serialize & deserialize】

class Codec:
    SEP = ","
    NULL = "#"

    # 主函数,将二叉树序列化为字符串
    def serialize(self, root):
        sb = []
        self._serialize(root, sb)
        return ''.join(sb)

    def _serialize(self, root, sb):
        if root is None:
            sb.append(self.NULL)
            sb.append(self.SEP)
            return
        
        self._serialize(root.left, sb)
        self._serialize(root.right, sb)

        # ****** 后序位置 ********
        sb.append(str(root.val))
        sb.append(self.SEP)
        # ***********************

    # 主函数,将字符串反序列化为二叉树结构
    def deserialize(self, data):
        # 将分割结果中的空字符串过滤掉
        nodes = [x for x in data.split(self.SEP) if x]
        return self._deserialize(nodes)

    # 辅助函数,通过 nodes 列表构造二叉树
    def _deserialize(self, nodes):
        if nodes == []:
            return None
        # 从后往前取出元素
        last = nodes.pop() 
        if last == self.NULL or last == "":
            return None
        root = TreeNode(int(last))
        # 先构造右子树,后构造左子树
        root.right = self._deserialize(nodes)
        root.left = self._deserialize(nodes)
        return root

3、层级遍历解法

先写出 二叉树遍历基础 中的层级遍历代码框架:

def traverse(root):
    if root is None:
        return
    # 初始化队列,将 root 加入队列
    q = [root]
    
    while q:
        sz = len(q)
        for i in range(sz):
            # 层级遍历代码位置
            cur = q.pop(0)
            print(cur.val)
            # *************
            if cur.left:
                q.append(cur.left)
            if cur.right:
                q.append(cur.right)

【serialize】

class Codec:
    SEP = ","
    NULL = "#"

    # 将二叉树序列化为字符串
    def serialize(self, root):
        if root is None:
            return ""
        # 初始化队列,将 root 加入队列
        queue = [root]
        res = []
        while queue:
            sz = len(queue)
            for i in range(sz):
                cur = queue.pop(0)

                # 层级遍历代码位置
                if cur is None:
                    res.append(self.NULL)
                    res.append(self.SEP)
                    continue
                res.append(str(cur.val))
                res.append(self.SEP)
                # ***************

                queue.append(cur.left)
                queue.append(cur.right)

        return Codec.SEP.join(res)

层级遍历序列化得出的结果如下图:

可以看到,每一个非空节点都会对应两个子节点,那么反序列化的思路也是用队列进行层级遍历,同时用索引 index 记录对应子节点的位置

【deserialize】

from collections import deque

class Codec:
    SEP = ","
    NULL = "#"

    # 将字符串反序列化为二叉树结构
    def deserialize(self, data: str):
        if data == "":
            return None
        # 将分割结果中的空字符串过滤掉
        nodes = [x for x in data.split(self.SEP) if x]
        # 第一个元素就是 root 的值
        root = TreeNode(int(nodes[0]))
        # 队列 q 记录父节点,将 root 加入队列
        q = deque([root])

        # index 变量记录正在序列化的节点在数组中的位置
        index = 1
        while q:
            sz = len(q)
            for i in range(sz):
                parent = q.popleft()
                # 为父节点构造左侧子节点
                left = nodes[index]
                index += 1
                if left != self.NULL:
                    parent.left = TreeNode(int(left))
                    q.append(parent.left)
                # 为父节点构造右侧子节点
                right = nodes[index]
                index += 1
                if right != self.NULL:
                    parent.right = TreeNode(int(right))
                    q.append(parent.right)
                    
        return root

不难发现,这个反序列化的代码逻辑也是标准的二叉树层级遍历的代码衍生出来的。我们的函数通过 nodes[index] 来计算左右子节点,接到父节点上并加入队列,一层一层地反序列化出来一棵二叉树。

总结

1、前序遍历和后序遍历的 serialize函数 就是个前序写法和后序写法的区别,而二者的 deserialize函数 本质上都为前序写法

核心区别在于:前者压缩时是“中左右”的顺序,故解压缩中从前向后遍历时,先构造root.left再构造root.right。后者压缩时顺序是“左右中”,故解压缩中从后向前遍历时,先构造root.right,再构造root.left。

2、层序遍历:

①经典的while + for框架中,while 遍历root的所有层,for遍历root某一层的所有节点。

②本题用层序遍历最妙的地方在于用nodes[index]指代某个节点的子节点,遍历完left就index++,遍历完right再index++,随着每次queue.pop(0),nodes[index]总是指向该节点的子节点,妙!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/873958.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity面向对象补全计划 之 List<T>与class(非基础)

C# & Unity 面向对象补全计划 泛型-CSDN博客 关于List,其本质就是C#封装好的一个数组,是一个很好用的轮子,所以并不需要什么特别说明 问题描述 假设我们有一个表示学生的类 Student,每个学生有姓名和年龄两个属性。我们需要创…

MFC工控项目实例之十二板卡测试信号输出界面

承接专栏《MFC工控项目实例之十一板卡测试信号输入界面》 1、在BoardTest.h文件中添加代码 CButtonST m_btnStart[16],m_btnStart_O[16];2、在BoardTest.cpp文件中添加代码 UINT No_IDC_CHECK_O[16] {IDC_CHECK16,IDC_CHECK17,IDC_CHECK18,IDC_CHECK19,IDC_CHECK20,IDC_CH…

Apache Guacamole 安装及配置VNC远程桌面控制

文章目录 官网简介支持多种协议无插件浏览器访问配置和管理应用场景 Podman 部署 Apache Guacamole拉取 docker 镜像docker-compose.yml部署 PostgreSQL生成 initdb.sql 脚本部署 guacamole Guacamole 基本用法配置 VNC 连接 Mac 电脑开启自带的 VNC 服务 官网 https://guacam…

华为防火墙 nat64

如果设备接收到的IPv6报文的前缀是设备为NAT64定义的前缀,说明报文的目的地址是IPv4网络,报文将经过NAT64处理后被转发至IPv4网络。 如果设备接收到的IPv6报文的前缀不是设备为NAT64定义的前缀,说明报文的目的地址是IPv6网络,报文…

大数据与人工智能:脑科学与人工神经网络ANN

文章目录 大数据与人工智能:脑科学与人工神经网络ANN一、引言ANN简介研究背景与应用领域发展背景应用场景 二、ANN背后的人脑神经网络人脑神经网络的专业描述神经元的结构信号处理 思考和认知过程认知功能的实现 对机器学习算法的启示 三、ANN的研究进展初始阶段&am…

idear获取git项目

最近想下载个ruoyi项目来包装简历,结果打开idear总是上一个项目,找不到get for vcs只好自己捣鼓了,顺便记录留着下次用。 步骤: 1. 2. 3.输入我们想访问的地址 eg: 点击克隆,我们就能获取项目到本地了。

【2024高教社杯全国大学生数学建模竞赛】B题模型建立求解

目录 1问题重述1.1问题背景1.2研究意义1.3具体问题 2总体分析3模型假设4符号说明(等四问全部更新完再写)5模型的建立与求解5.1问题一模型的建立与求解5.1.1问题的具体分析5.1.2模型的准备 目前B题第一问的详细求解过程以及对应论文部分已经完成&#xff…

CSDN文章无水印转成PDF

文章目录 一、打开检查二、点击进入控制台三、在控制台中输入代码 一、打开检查 f11或者右键打开检查 二、点击进入控制台 三、在控制台中输入代码 (function(){ use strict;var articleBox $("div.article_content");articleBox.removeAttr("style&quo…

matlab和opencv在双目标定参数之间的关系,不用转置和加负号

用matlab标定相机参数,并应用于opencv以提高精度 opencv的相机标定,每张图片的误差显示不出来,但是matlab比较清晰,有每张图片的矫正结果、误差、相机位姿等显式的结果,而且结果往往比opencv的例程更可靠一点&#xff…

虚拟现实智能家居实训系统实训解决方案

随着科技的飞速发展,智能家居已成为现代生活的重要组成部分,它不仅极大地提升了居住的便捷性与舒适度,还推动了物联网、大数据、人工智能等前沿技术的融合应用。为了满足市场对智能家居专业人才日益增长的需求,虚拟现实智能家居实…

【有啥问啥】HashHop在LTM-2-mini中的应用:解锁长期记忆模型的新纪元

HashHop在LTM-2-mini中的应用:解锁长期记忆模型的新纪元 引言 随着AI技术的飞速发展,模型在处理复杂任务和数据时所需的上下文窗口大小也在不断扩展。深度学习模型在处理超长上下文时,往往面临着计算资源消耗高、上下文丢失等问题。近期&am…

前端开发的单例设计模式

一、什么是单例模式 单例模式(Singleton Pattern)是一种常见的设计模式,它确保在整个应用程序的生命周期中,一个类只能创建一个实例。无论你在代码的任何地方尝试创建该类的新实例,它都会返回已经存在的唯一实例。这在…

【通信管理之c++基础01】std::future

std::future https://en.cppreference.com/w/cpp/thread/future https://cplusplus.com/reference/future/future/ std::async #

【C++】C++入门基础,详细介绍命名空间,缺省参数,函数重载,引用,内联函数等

目录 1. 命名空间 1.1 使用命名空间的目的 1.2 命名空间定义 1.3 命名空间使用 2. 缺省参数 2.1 缺省参数概念 2.2 缺省参数分类 2.3 实际案例 2.4 注意事项 3. 函数重载 3.1 函数重载概念 3.2 函数重载原理 4. 引用 4.1 引用的概念 4.2 引用的特性 4.3 使用…

(计算机网络)运输层

一.运输层的作用 运输层:负责将数据统一的交给网络层 实质:进程在通信 TCP(有反馈)UDP(无反馈) 二.复用和分用 三. TCP和UDP的特点和区别 进程号--不是固定的 端口号固定--mysql--3306 端口--通信的终点 …

【重学 MySQL】十二、SQL 语言的规则与规范

【重学 MySQL】十二、SQL 语言的规则与规范 基本规则注释语法规则命名规则基本命名规则具体命名规范其他注意事项 数据导入指令 SQL(Structured Query Language,结构化查询语言)的规则与规范是确保SQL语句能够正确执行、提高代码可读性和可维…

数据结构C //线性表ADT结构及相关函数

数据结构(C语言版)严蔚敏 吴伟民 线性表ADT结构及相关函数 环境:Linux Ubuntu(云服务器) 工具:vim 代码块(头文件,函数文件,主文件) list.h头文件 /****…

11大排序的原理讲解和Python源码剖析

排序算法 【谁教你这么剪的 | 11大排序的原理讲解和Python源码剖析】 https://www.bilibili.com/video/BV1Zs4y1X7mN/?share_sourcecopy_web&vd_sourceed4a51d52f6e5c9a2cb7def6fa64ad6a 稳定:如果a原本在b前面,而ab,排序之后a仍然在b…

金士顿NV2 2TB假固态硬盘抢救记,RL6577/RTS5765DL量产工具,RTS5765DL+B47R扩容开卡修复

之前因为很长时间不买固态硬盘,没注意到NVME的固态盘也有了假货和扩容盘,花200多块买了个2TB的金士顿NV2固态硬盘,我原本以为NV1的假货最多是用黑片冒充正片,结果没想到NV2居然有扩容的。后来发现是扩容盘的时候,已经过…

亿发进销存一体化解决方案:数据互联互通,优化企业全局管理-下

亿发软件凭借对产品、市场、业务的深入理解,在进销存基础上进行了延伸,推出多终端、一体化的“进销存管理系统”多元产品矩阵。在技术上实现电脑端、手机端、PDA端、零售端、商家版以及小程序商城的多终端无缝对接,保障企业业务的连贯性和效率…