LeetCode 297. Serialize and Deserialize Binary Tree【树,DFS,BFS,设计,二叉树,字符串】困难

news2024/12/24 9:32:38

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

0 - 6 个月:字节跳动 3、Facebook 2、亚马逊 2、雅虎 Yahoo 2、特斯拉 2

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例 1:

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

示例 4:

输入:root = [1,2]
输出:[1,2]

提示:

  • 树中结点数在范围 [0, 10^4] 内
  • -1000 <= Node.val <= 1000

类似题目:

  • 449. 序列化和反序列化二叉搜索树
  • 297. 二叉树的序列化与反序列化 困难
  • 428. 序列化和反序列化 N 叉树 困难

二叉树的序列化本质上是对其值进行编码,更重要的是对其结构进行编码。可以遍历树来完成上述任务。众所周知,我们一般有两个策略:广度优先搜索和深度优先搜索。

下面给出BFS和DFS做法。并借这道题稍微介绍一下拼接字符串的神器,`StringJoiner 类。 S t r i n g J o i n e r StringJoiner StringJoiner 的两种主要用法:

  1. StringJoiner sj = new StringJoiner(",", "[", "]"); 第一个参数表示拼接对象之间的连接符,第二个参数表示拼接后的前缀,第三个参数表示拼接后的后缀。例如将 sj.add("a"); sj.add("b") 之后sj.toString()"[a,b]"
  2. StringJoiner sj = new StringJoiner(","); 相比1,不指定前缀和后缀,上述例子拼接后为 "a,b"

解法1 广度优先搜索

最直接的做法是BFS。对于序列化,通过队列将结点数值依次拼成一个字符串。对当前出队结点 h e a d head head ,考察其左右儿子,若有,则将数字转为字符串后拼接,若无,则拼接 null由于题目并不要求固定的格式,只要我们能从序列化后的字符串反序列化出树即可,因此序列化拼接形式是自由的。可以采用 StringBuilderStringJoiner ,后者内部调用了 StringBuilder ,更便于格式化拼接。「代码」中使用 StringJoiner 完成拼接。

对于反序列化做法类似,也借助队列通过BFS方式完成。先将输入转为数组,利用 idx 跟踪当前反序列化的结点。首节点入队后,进入while循环,结点依次出队,idx 总是依次指向出队 head 结点的左右儿子,若 idx 指向的字符串不为 null ,则将其反序列化为结点,然后将 head 相应儿子指向它。这里用idx < n n n n 是结点字符串数组的大小)来作为while的循环条件。

import java.util.StringJoiner;
public class Codec {
    public String serialize(TreeNode root) {
        if (root == null) return "";
        Queue<TreeNode> q = new ArrayDeque<>();
        StringJoiner sj = new StringJoiner(",");
        q.add(root);
        sj.add(Integer.toString(root.val));
        while (!q.isEmpty()) {
            TreeNode head = q.remove();
            if (head.left != null) {
                q.add(head.left);
                sj.add(Integer.toString(head.left.val));
            } else sj.add("null");
            if (head.right != null) {
                q.add(head.right);
                    sj.add(Integer.toString(head.right.val));
            } else sj.add("null");
        }
        return sj.toString();
    }
    public TreeNode deserialize(String data) {
        if (data.length() == 0) return null; // 特判:data == ""
        String[] nodes = data.split(",");
        Queue<TreeNode> q = new ArrayDeque<>();
        TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
        q.add(root);
        int idx = 1, n = nodes.length;
        while (idx < n) { // 不必以!q.isEmpty()作为判断条件
            TreeNode head = q.remove();
            if (!nodes[idx].equals("null")) {
                TreeNode left = new TreeNode(Integer.parseInt(nodes[idx])); 
                head.left = left; // left挂接到head
                q.add(left);
            } 
            idx++;
            if (!nodes[idx].equals("null")) {
                TreeNode right = new TreeNode(Integer.parseInt(nodes[idx])); 
                head.right = right; // right挂接到head
                q.add(right);
            } 
            idx++;
        }
        return root;
    }
}

解法2 深度优先搜索

广度优先搜索可以按照层次的顺序从上到下遍历所有的节点,深度优先搜索可以从一个根开始,一直延伸到某个叶,然后回到根,到达另一个分支。根据根节点、左节点和右节点之间的相对顺序,可以进一步将深度优先搜索策略区分为:

  • 先序遍历
  • 中序遍历
  • 后序遍历

这里,我们选择先序遍历的编码方式,通过这样一个例子简单理解:

  1. 我们从根节点 1 1 1 开始,序列化字符串是 1 , 1, 1,
  2. 然后我们跳到根节点 2 2 2 的左子树,序列化字符串变成 1 , 2 , 1,2, 1,2, 。现在从节点 2 2 2 开始,我们访问它的左节点 3 3 3 1 , 2 , 3 , N o n e , N o n e , 1,2,3,None,None, 1,2,3,None,None,)和右节点 4 ( 1 , 2 , 3 , N o n e , N o n e , 4 , N o n e , N o n e ) 4(1,2,3,None,None,4,None,None) 4(1,2,3,None,None,4,None,None) N o n e , N o n e , None,None, None,None, 是用来标记缺少左、右子节点,这就是我们在序列化期间保存树结构的方式
  3. 最后,我们回到根节点 1 1 1 并访问它的右子树,它恰好是叶节点 5 5 5 。最后,序列化字符串是 1 , 2 , 3 , N o n e , N o n e , 4 , N o n e , N o n e , 5 , N o n e , N o n e , 1,2,3,None,None,4,None,None,5,None,None, 1,2,3,None,None,4,None,None,5,None,None,

我们可以先序遍历这颗二叉树,遇到空子树的时候序列化成 N o n e None None ,否则继续递归序列化。那么我们如何反序列化呢?首先我们需要根据 , 把原先的序列分割开来得到先序遍历的元素列表,然后从左向右遍历这个序列:

  1. 如果当前的元素为 N o n e None None ,则当前为空树
  2. 否则先解析这棵树的左子树,再解析它的右子树
public class Codec {
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        return serialize(root, new StringBuilder()).toString();
    }
    private StringBuilder serialize(TreeNode root, StringBuilder str) {
        if (root == null) str.append("None,");
        else {
            str.append(String.valueOf(root.val) + ",");
            str = serialize(root.left, str);
            str = serialize(root.right, str);
        }
        return str;
    }

    private Integer index;
    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        String[] tokenArray = data.split(",");
        index = 0;
        return deserialize(tokenArray);
    }
    public TreeNode deserialize(String[] data) {
        if (data[index].equals("None")) {
            ++index;
            return null;
        }
        TreeNode root = new TreeNode(Integer.valueOf(data[index]));
        ++index;
        root.left = deserialize(data);
        root.right = deserialize(data);
        return root;
    }
}

复杂度分析:

  • 时间复杂度:在序列化和反序列化函数中,我们只访问每个节点一次,因此时间复杂度为 O ( n ) O(n) O(n) ,其中 n n n 是节点数,即树的大小。
  • 空间复杂度:在序列化和反序列化函数中,我们递归会使用栈空间,故渐进空间复杂度为 O ( n ) O(n) O(n)

解法3 括号表示编码 + 递归下降解码

我们也可以这样表示一颗二叉树:

  1. 如果当前的树为空,则表示为 X X X
  2. 如果当前的树不为空,则表示为 (<LEFT_SUB_TREE>)CUR_NUM(RIGHT_SUB_TREE) ,其中:
    1. <LEFT_SUB_TREE> 是左子树序列化之后的结果
    2. <RIGHT_SUB_TREE> 是右子树序列化之后的结果
    3. CUR_NUM 是当前节点的值

根据这样的定义,我们很好写出序列化的过程,后序遍历这颗二叉树即可,那如何反序列化呢?根据定义,我们可以推导出这样的巴科斯范式(BNF):
T − > ( T ) n u m ( T )   ∣   X T -> (T) num (T)\ |\ X T>(T)num(T)  X
它的意义是:用 T T T 代表一棵树序列化之后的结果, ∣ | 表示 T T T 的构成为 ( T ) n u m ( T ) (T) num (T) (T)num(T) 或者 X X X ∣ | 左边是对 T T T 的递归定义,右边规定了递归终止的边界条件

因为:

  • T T T 的定义中,序列中的第一个字符要么是 X X X ,要么是 ( ( ( ,所以这个定义是不含左递归的
  • 当我们开始解析一个字符串的时候,如果开头是 X X X我们就知道这一定是解析一个「空树」的结构
  • 如果开头是 ( ,我们就知道需要解析 ( T ) n u m ( T ) (T) num (T) (T)num(T) 的结构,
  • 因此这里两种开头和两种解析方法一一对应,可以确定这是一个无二义性的文法

我们只需要通过开头的第一个字母 X X X 还是 ( ( ( 来判断使用哪一种解析方法。所以这个文法是 L L ( 1 ) LL(1) LL(1) 型文法,如果你不知道什么是 L L ( 1 ) LL(1) LL(1) 型文法也没有关系,只需要知道它定义了一种递归的方法来反序列化,也保证了这个方法的正确性——我们可以设计一个递归函数:

  • 这个递归函数传入两个参数,带解析的字符串和当前当解析的位置 p t r ptr ptr p t r ptr ptr 之前的位置是已经解析的, p t r ptr ptr p t r ptr ptr 后面的字符串是待解析的
  • 如果当前位置为 X X X 说明解析到了一棵空树,直接返回
  • 否则当前位置一定是 ( ( ( ,对括号内部按照 ( T ) n u m ( T ) (T) num (T) (T)num(T) 的模式解析

具体请参考下面的代码。

public class Codec {
    public String serialize(TreeNode root) {
        if (root == null) return "X";
        String left = "(" + serialize(root.left) + ")";
        String right = "(" + serialize(root.right) + ")";
        return left + root.val + right;
    }
    private int ptr;
    public TreeNode deserialize(String data) {
        ptr = 0;
        return parse(data);
    }
    public TreeNode parse(String data) {
        if (data.charAt(ptr) == 'X') {
            ++ptr;
            return null;
        }
        TreeNode cur = new TreeNode(0);
        cur.left = parseSubtree(data);
        cur.val = parseInt(data);
        cur.right = parseSubtree(data);
        return cur;
    }
    public TreeNode parseSubtree(String data) {
        ++ptr; // 跳过左括号
        TreeNode subtree = parse(data);
        ++ptr; // 跳过右括号
        return subtree;
    }
    public int parseInt(String data) {
        int x = 0, sgn = 1;
        if (!Character.isDigit(data.charAt(ptr))) {
            sgn = -1;
            ++ptr;
        }
        while (Character.isDigit(data.charAt(ptr))) 
            x = x * 10 + data.charAt(ptr++) - '0';
        return x * sgn;
    }
}

复杂度分析:

  • 时间复杂度:序列化时做了一次遍历,渐进时间复杂度为 O ( n ) O(n) O(n) 。反序列化时,在解析字符串的时候 p t r ptr ptr 指针对字符串做了一次顺序遍历,字符串长度为 O ( n ) O(n) O(n) ,故这里的渐进时间复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度:考虑递归使用的栈空间的大小,这里栈空间的使用和递归深度有关,递归深度又和二叉树的深度有关,在最差情况下,二叉树退化成一条链,故这里的渐进空间复杂度为 O ( n ) O(n) O(n)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode 428. Serialize and Deserialize N-ary Tree【树,BFS,DFS】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

华为倒逼苹果?走出舒适圈积极创新,苹果推出首批CIS堆叠式手机

9月9日消息&#xff0c;苹果公司最近推出了iPhone 15系列&#xff0c;这是业内首批搭载CIS堆叠式传感器的手机。这一消息得到了相关业内人士的关注。知名分析师郭明錤认为&#xff0c;尽管美国施加了制裁&#xff0c;在一些方面对苹果的影响是负面的&#xff0c;但华为的归来将…

Spring-MVC的文件上传下载,及插件的使用(让项目开发更节省时间)

目录 一、概述 ( 1 ) 介绍 ( 2 ) 讲述 二、上传 三、下载 四、jrebel的使用 五、多文件上传 给我们带来什么收获 一、概述 ( 1 ) 介绍 Spring MVC的文件上传下载是指在Spring MVC框架中实现文件的上传和下载功能。文件上传是指将本地计算机上的文件上传到服务器端…

一种基于注意机制的快速、鲁棒的混合气体识别和浓度检测算法,配备了具有双损失函数的递归神经网络

A fast and robust mixture gases identification and concentration detection algorithm based on attention mechanism equipped recurrent neural network with double loss function 摘要 提出一个由注意力机制组成的电子鼻系统。首先采用端到端的编码器译码器&#xff…

987. 二叉树的垂序遍历

987. 二叉树的垂序遍历 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a;对中文的支持有点不友好IDEA 原题链接&#xff1a; 987. 二叉树的垂序遍历 https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/descriptio…

2.3 Java中的运算符

运算符&#xff0c;是表示各种不同运算的符号。 运算符按功能分为算术运算符、逻辑运算符、关系运算符、赋值运算符、位运算符等。 1. 算术运算符 算术运算符一般用于对整型数和浮点型数运算。 运算符 运算 范例 结果 正号 3 3 - 负号 b4;-b -4 加 55 10 …

恒运资本:开盘时间是几点到几点?

开盘时刻是指各种商场的正式开端生意时刻&#xff0c;包括股票商场、外汇商场、商品期货商场等。关于出资者来说&#xff0c;了解开盘时刻是十分重要的&#xff0c;由于它直接关系到生意的时刻和机会。本文将从多个角度分析开盘时刻的重要性、不同商场的开盘时刻以及对出资者带…

linux环境部署jmeter并执行测试

下载jmeter和jdk jmeter官网和java-jdk官网下载压缩包文件 jmeter下载地址&#xff1a;点此下载 jmeter Apache JMeter - Download Apache JMeter java-jdk下载地址&#xff1a;点此下载 jdk Java Downloads | Oracle 安装包根据Linux配置进行选择。 上传文件到Linux并解压文…

大厂案例 - 海量分类业务设计的一些思考

文章目录 业务背景描述方案演进v1 扩展字段扩展性需求查询需求 v2 垂直拆分拆分方案存在的问题 v3 三大中心服务 &#xff08;业界最佳实践&#xff09;统一帖子中心服务统一类目属性服务统一检索服务 小结 业务背景描述 一个分类信息平台&#xff0c;有很多垂直品类&#xff…

LeetCode(力扣)47.全排列 IIPython

LeetCode47.全排列 II 题目链接代码 题目链接 https://leetcode.cn/problems/permutations-ii/ 代码 class Solution:def permuteUnique(self, nums):nums.sort() # 排序result []self.backtracking(nums, [], [False] * len(nums), result)return resultdef backtrackin…

【Python】多线程

进程、线程 现代操作系统比如Mac OS X&#xff0c;UNIX&#xff0c;Linux&#xff0c;Windows等&#xff0c;都是支持“多任务”的操作系统。 进程&#xff1a;就是一个程序&#xff0c;运行在系统之上&#xff0c;那么便称之这个程序为一个运行进程&#xff0c;并分配进程ID…

JavaScript学习笔记02

JavaScript笔记02 数据类型详解 字符串 在 JavaScript 中正常的字符串都使用单引号 或者双引号" "包裹&#xff1a;例&#xff1a; 转义字符 在 JavaScript 字符串中也可用使用转义字符&#xff08;参考&#xff1a;详解转义字符&#xff09;&#xff1a;例&…

电工什么是电动势

什么是电动势&#xff1f;及电源电动势计算公式与方向确定 前面我们讲到在基本电路中的电流和电压的基础知识&#xff0c;而本文要讲的电动势和电压是一个很类似的概念。那么什么是电动势&#xff1f;电源电动势的计算公式是什么&#xff1f;它的方向如何确定及与电压有什么区…

【C语言】找单身狗问题

一.找单身狗问题初阶 1.问题描述 一个数组中只有一个数字是出现一次,其他所有数字都出现了两次.编写一个函数,找出这个只出现一次的数字. 例如: 有数组的元素是:1,2,3,4,5,1,2,3,4 只有5出现了一次,要找出5. 2.解题思路 常规思路: 在常规思路中,我们首先想到的肯定是使用两层…

腾讯云新用户有哪些优惠政策和专属活动?

腾讯云作为中国领先的云计算服务提供商&#xff0c;一直在为用户提供优质、高效且具有竞争力的服务。对于新用户&#xff0c;腾讯云更是诚意满满&#xff0c;推出了一系列优惠政策和专属活动。本文将详细介绍腾讯云新用户的优惠政策和专属活动&#xff0c;帮助大家更好地了解如…

ArcGIS 10.6安装教程!

软件介绍&#xff1a;ArcGIS是一款专业的电子地图信息编辑和开发软件&#xff0c;提供一种快速并且使用简单的方式浏览地理信息&#xff0c;无论是2D还是3D的信息。软件内置多种编辑工具&#xff0c;可以轻松的完成地图生产全过程&#xff0c;为地图分析和处理提供了新的解决方…

android注解之APT和javapoet

前言 前面我们已经讲过注解的基本知识&#xff0c;对于注解还不太了解的&#xff0c;可以去看一下之前的文章&#xff0c; android 注解详解_袁震的博客-CSDN博客。 之前我们在讲注解的时候&#xff0c;提到过APT和JavaPoet&#xff0c;那么什么是APT和JavaPoet呢&#xff1…

pprof火焰图性能优化

pprof火焰图性能优化 火焰图&#xff08;flame graph&#xff09;是性能分析的利器,在go1.1之前的版本我们需要借助go-torch生成,在go1.1后go tool pprof集成了此功能,今天就来说说如何使用其进行性能优化 在你启动http server的地方直接加入导入: _ “net/http/pprof” 获取…

如何在 7 分钟内黑掉 40 个网站

这将详细讲述我如何侵入托管 40 个(这是一个确切数字)网站的服务器以及我的发现。 注意:需要一些必备的 CS 知识才能理解本文的技术部分。 一位朋友给我发消息说他的网站发现了XSS 漏洞,希望我进一步查看。这是一个重要的阶段,因为我倾向于要求他正式表示我已获得他的许可…

Mojo 摸脚语言,似乎已经可以安装

文章原地址&#xff1a;https://i.scwy.net/it/2023/090821-mojo/ Mojo 吹得很凶&#xff0c;面向AI编程&#xff0c;甩Python几十条街&#xff0c;融资上亿.... 但无缘一试&#xff0c;在Win和Ubuntu上试都不能通过。 由 LLVM 和 Swift 编程语言的联合创始人 Chris Lattner…