本篇概览

  • 因为欣宸个人水平有限,在刷题时一直不敢面对hard级别的题目,生怕出现一杯茶一包烟,一道hard做一天的窘境

  • 这种恐惧心理一直在,直到遇见了它:LeetCode297,建议不敢做hard题的新手们速来围观,拿它练手,轻松找到自信

题目简介

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

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

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

树中结点数在范围 [0, 104] 内
-1000 <= Node.val <= 1000
  • 接下来,先开始轻松愉快的分析工作

分析

  • 小结一下题目要求,需要做两件事:
  1. serialize方法:输入二叉树根节点,返回字符串
  2. deserialize方法:输入字符串,方法内部根据字符串构建一棵二叉树,然后返回根节点
  • 说实话,当时读题后的第一反应是:这是二叉树的基本操作嘛,一定是个easy,结果发现官方设定的难度是hard,当时就觉得赚大了!!!
  • 简单的说,解题思路只有四个字:前序遍历
  • 前序遍历是啥?很简单,是指一种遍历二叉树的顺序,看着下面的图,咱们一起默念:根左右,所以前序遍历下图二叉树的结果是:1,2,3

  • 类似的还有中序遍历,中序遍历要求根节点在输出位置的中间,也就是左根右,还是上面那个二叉树,中序遍历的结果是:2,1,3
  • 还有后续遍历是左右根,上面那个二叉树,后序遍历的结果是:2,3,1
  • 至于本题为何要选前序遍历,因为字符串转二叉树时,会涉及到数组,而将根节点放在数组的最前面,这样既便于处理也便于理解
  • 再来看看前序遍历的代码一般怎么写?或者说套路是什么?
  • 伪代码如下,可见是个非常简单的递归操作
private void dfs(TreeNode root) {
// 终止条件是发现入参为空
if(null==root) {
return;
} // 1. 根
处理root的代码
// 2. 左
dfs(root.left);
// 3. 右
dfs(root.right);
}
  • 以前面那个图上的二叉树为例,分析上述代码如何执行:调用dfs的时候传入的是根节点,在dfs方法中,处理完根节点后,立即调用dfs处理左节点,在处理左节点的时候还会再递归一次,不让过左节点的子节点都是null,所以这个递归啥事也没做,处理完左节点后再调用dfs处理右节点,这样就完成了根左右的处理
  • 没错,二叉树遍历的套路就是这么简单,至于中序和后续遍历的代码,和上面的差不多,无非是将三段代码的调用顺序调整一下即可
  • 接下来,编码

编码:序列化

  • 先看序列化的代码
  • 首先准备一个StringBuilder类型的成员变量serializeRes,遍历到的每一个元素都存放在serializeRes的尾部,用逗号分隔
private StringBuilder serializeRes;
  • 然后是serialize方法的实现,首先要判断root为空的特殊情况,另外就是serializeRes的初始化,然后就会调用serializeDfs方法,这个serializeDfs就是遍历二叉树的具体实现
public String serialize(TreeNode root) {
if (null==root) {
return null;
} serializeRes = new StringBuilder();
serializeDfs(root);
return serializeRes.toString();
}
  • 遍历二叉树的核心逻辑,serializeDfs方法如下,可见和咱们刚才的伪代码很像,每一个节点都会被存入serializeRes,并且以逗号分隔,只有一处需要注意,就是每当遇到root等于null,就在serializeRes尾部追加一个n,这样在serializeRes中就相当于每个节点没有左子节点或者右子节点的标志,这很重要!
private void serializeDfs(TreeNode root) {
if(null==root) {
serializeRes.append("n,");
return;
} // 1. 根
serializeRes.append(root.val).append(",");
// 2. 左
serializeDfs(root.left);
// 3. 右
serializeDfs(root.right);
}
  • 以前面图中那个最简单的二叉树为例是,上述代码输出的字符串内容如下,3在2,n,n之后,显然是将2和2的子节点都处理完毕后,才去处理3,这就是典型的根左右:
1,2,n,n,3,n,n,

编码:反序列化

  • 接下来的反序列化,也是严格准守根左右的顺序实现的,关键是注意对字符n的处理,它表示一个节点没有左子节点或者右子节点了
  • 首先是两个环境变量,deserializeArray是个数组,字符串用逗号分割之后生成的数组,代表整个要恢复的二叉树的所有元素,deserializeOffset用于记录数组中已经有多少个元素已经被回复到二叉树中了:
  private String[] deserializeArray;

  private int deserializeOffset;
  • 然后是最外层的发序列化方法,可见首先是处理异常逻辑,然后就会用字符串分割生成数组,再调用构建二叉树的核心逻辑deserializeDfs:
    public TreeNode deserialize(String data) {
if (null==data) {
return null;
} deserializeArray = data.split(",");
deserializeOffset = 0;
return deserializeDfs();
}
  • 最后看构建二叉树的核心逻辑deserializeDfs方法,不要以为构建二叉树的代码会比遍历二叉树的代码复杂,仔细看,发现还是严格准守根左右的顺序去处理的,先生成根节点,然后递归生产左子树和右子树,要注意的地方就是遇到字符n的时候就不要继续递归了,深度上已经到底了,需要返回上层,完成上层左子节点和右子节点的创建:
private TreeNode deserializeDfs() {
if (deserializeOffset>=deserializeArray.length) {
return null;
} if ("n".equals(deserializeArray[deserializeOffset])) {
deserializeOffset++;
return null;
} // 1. 根
TreeNode treeNode = new TreeNode(Integer.valueOf(deserializeArray[deserializeOffset++]));
// 2. 左
treeNode.left = deserializeDfs();
// 3. 右
treeNode.right = deserializeDfs(); return treeNode;
}
  • 最后贴出完整的代码
public class Codec {

    // 本题的整体思路是前序遍历,即:根左右
private StringBuilder serializeRes; private String[] deserializeArray; private int deserializeOffset; private void serializeDfs(TreeNode root) {
if(null==root) {
serializeRes.append("n,");
return;
} // 1. 根
serializeRes.append(root.val).append(",");
// 2. 左
serializeDfs(root.left);
// 3. 右
serializeDfs(root.right);
} private TreeNode deserializeDfs() {
if (deserializeOffset>=deserializeArray.length) {
return null;
} if ("n".equals(deserializeArray[deserializeOffset])) {
deserializeOffset++;
return null;
} // 1. 根
TreeNode treeNode = new TreeNode(Integer.valueOf(deserializeArray[deserializeOffset++]));
// 2. 左
treeNode.left = deserializeDfs();
// 3. 右
treeNode.right = deserializeDfs(); return treeNode;
} // Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (null==root) {
return null;
} serializeRes = new StringBuilder();
serializeDfs(root);
return serializeRes.toString();
} // Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (null==data) {
return null;
} deserializeArray = data.split(",");
deserializeOffset = 0;
return deserializeDfs();
}
}
  • 提交代码,如下图,顺利AC,速度超97%,同时内存超93%,感觉美滋滋的,这可个是一道hard呀!

小幅度优化

  • 回顾此题,似乎还有一丁点优化空间:在序列化的时候,咱们用字符n作为子节点为空的标志,例如
1,2,n,n,3,n,n,
  • 如果用空字符串取代n,那岂不是省掉了一些空间?
  • 说干就干,一共有两处,第一处在序列化的时候,用n做结束标志的那段代码,改动如下图

  • 第二处是反序列化的时候,判断是否为n的那段代码,改动如下图

  • 改完提交代码,效果如下图,速度和内存都有小幅度优化,第一次距离双百这么近!

  • 至此,297的分析和实战已经完成,hard题能如此简单,实属不易遇到,所以不要错误哦,希望本文能给您一些思路,助您用最基础的代码,跑出最耀眼的成绩

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

LeetCode297:hard级别中最简单的存在,java版,用时击败98%,内存击败百分之九十九的更多相关文章

  1. 简单聊天室(java版)

    这是本人从其他地方学习到的关于聊天室的一个模本,我从中截取了一部分关于客户端和服务端通信的Socket的内容.希望对大家对socket有个了解,我写的这些代码可以实现两人或多人在多台电脑上实现简单的对 ...

  2. 菜鸟学Java(六)——简单验证码生成(Java版)

    验证码大家都知道,它的作用也不用我多说了吧.如果不太清楚请参见百度百科中的解释,一般验证码的生成就是随机产生字符(数字.字母或者汉字等),然后将这些生成的字符绘制成一张图片,再在图片上加上一些干扰元素 ...

  3. etcd简单测试类java版

    为了方便现场安装完了etcd集群后确认集群是否好用,简单写了个测试类,网上搜的有点乱还有些不能运行,在这里再整理一个能够直接运行的 1.我把etcd的API设成3版本了,调用使用的jetcd,功能挺多 ...

  4. 【线性表基础】基于线性表的简单算法【Java版】

    本文描述了基于线性表的简单算法及其代码[Java实现] 1-1 删除单链表中所有重复元素 // Example 1-1 删除单链表中所有重复元素 private static void removeR ...

  5. RAS算法简单示例(Java版)

    RSA算法——由三位发明者Ronald Rivest.Adi Shamir 和 Leonard Adleman 姓氏的首字母拼在一起组成. RSA算法属于“公开密钥加密技术”,其加密和解密的秘钥不同. ...

  6. 剑指offer:1.找出数组中重复的数(java版)

    数组中重复的数:题目:找出数组中重复的数,题目描述:在一个长度为n的数组里的所有数字都在0到n-1的范围内.数组中某些数字是重复的,但不知道有几个数字是重复的.也不知道每个数字重复几次.请找出数组中任 ...

  7. 剑指offer第二版面试题2:数组中重复的数字(JAVA版)

    题目:在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的.请找出数组中任意一个重复的数字,但是不能修改输入的数组.例如,如果输入长度为8的数组{2,3,5,4,3 ...

  8. 剑指offer第二版面试题1:数组中重复的数字(JAVA版)

    题目:在一个长度为n的数组里的所有数字都在0到n-1的范围内.数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复的次数.请找出数组中任意一个重复的数字.例如如果输入长度为7的数组{ ...

  9. MongoDB简单操作(java版)

    新建maven项目,添加依赖: <dependency> <groupId>org.mongodb</groupId> <artifactId>mong ...

  10. Java中的简单工厂模式

    举两个例子以快速明白Java中的简单 工厂模式: 女娲抟土造人话说:“天地开辟,未有人民,女娲抟土为人.”女娲需要用土造出一个个的人,但在女娲造出人之前,人的概念只存在于女娲的思想里面.女娲造人,这就 ...

随机推荐

  1. 代码随想录算法训练营Day5 数组、链表复习

    数组部分 数组最重要的思维方式是双指针的使用. 快慢指针 在进行元素移除和元素操作时会使用两个for循环嵌套,此时时间复杂度为O(n²).在for循环中通过双指针(快慢指针)的使用可以使时间复杂度将为 ...

  2. 【最佳实践】如何设计 RESTful API ?

    良好的 API 设计是一个经常被提及的话题,特别是对于那些试图完善其 API 策略的团队来说.一个设计良好的 API 的好处包括:改进开发者体验.更快速地编写文档以及更高效地推广你的 API.但是,到 ...

  3. 图扑虚拟现实 VR 智慧办公室可视化

    前言 "虚拟现实"是来自英文"Virtual Reality",简称 VR 技术,其是通过利用计算机仿真系统模拟外界环境,主要模拟对象有环境.技能.传感设备和感 ...

  4. karyoploteR: 基因组数据可视化 R 包

    karyoploteR,是一个适用于所有基因组数据(any data on any genome)非圆环布局(non-circular layouts)的可视化 R/Bioconductor 包.开发 ...

  5. 使用CNI网络插件(calico)实现docker容器跨主机互联

    目录 一.系统环境 二.前言 三.CNI网络插件简介 四.常见的几种CNI网络插件对比 五.Calico网络之间是如何通信的 六.配置calico让物理机A上的docker容器c1可以访问物理机B上的 ...

  6. Python自学指南-第一章-安装运行

    1.1 [环境]快速安装 Python 与PyCharm "工欲善其事,必先利其器",为了自学之路的顺利顺利进行.首先需要搭建项目的开发环境. 1. 下载解释器 进入 Python ...

  7. Java杂记————object.getClass()和object.class以及Java中的toString()方法的的区别

    不说废话,直接上干货: (注意大小写:object为对象,Object为类) 1,object.getClass()它是Object类的实例方法,返回一个对象运行时的类的Class对象,换句话说,它返 ...

  8. 使用C#编写.NET分析器(三)

    译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断).IDE.诊断 ...

  9. P3755 [CQOI2017]老C的任务题解

    如果询问 \(x_1, y_1, x_2, y_2\), 那么询问 \((x_2, y_2)\), \((x_2, y_1 - 1)\), \((x_1 - 1, y_2)\) \((x_1 - 1, ...

  10. 【Docker】部署Tomcat

    搜索镜像 $ docker search 镜像名称:镜像TAG # 如: 没有加TAG,表示默认搜索的是最新版本的tomcat镜像 $ docker search tomcat # 如:搜索 tomc ...