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

  • 前言
  • 题目难度:困难
  • 一、题目描述
  • 二、前序遍历解法
  • 三、后序遍历解法
  • 四、中序遍历解法
  • 五、层级遍历解法

前言

如果你看过下面这几篇,那这道题应该对你来说信手拈来

深入理解二叉树的前中后序

104二叉树的最大深度,543二叉树的直径,124二叉树的最大路径和

226翻转二叉树,116填充二叉树节点的右侧指针,114将二叉树展开为链表

654最大二叉树,105根据前序与中序遍历构造二叉树,106根据中序与后序遍列构造二叉树,889根据前序和后序遍历构造二叉树

题目难度:困难

297. 二叉树的序列化与反序列化,leetcode链接

/*** Definition for a binary tree node.* public class TreeNode {*     int val;*     TreeNode left;*     TreeNode right;*     TreeNode(int x) { val = x; }* }*/
public class Codec {// Encodes a tree to a single string.public String serialize(TreeNode root) {}// Decodes your encoded data to tree.public TreeNode deserialize(String data) {}
}

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

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

那么假设现在有一棵用 Java 实现的二叉树,我想把它序列化字符串,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?

这就需要对二叉树进行「序列化」和「反序列化」了。

一、题目描述

力扣第 297 题「二叉树的序列化与反序列化」就是给你输入一棵二叉树的根节点 root,要求你实现如下一个类:

public class Codec {// 把一棵二叉树序列化成字符串public String serialize(TreeNode root) {}// 把字符串反序列化成二叉树public TreeNode deserialize(String data) {}
}

我们可以用 serialize 方法将二叉树序列化成字符串,用 deserialize 方法将序列化的字符串反序列化成二叉树,至于以什么格式序列化和反序列化,这个完全由你决定。

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

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

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

二叉树的遍历方式有哪些?

递归遍历方式有前序遍历,中序遍历,后序遍历;

迭代方式一般是层级遍历。

本文就把这些方式都尝试一遍,来实现 serialize 方法和 deserialize 方法。

二、前序遍历解法

前文 学习数据结构和算法的框架思维 说过了二叉树的几种遍历方式,前序遍历框架如下:

void traverse(TreeNode root) {if (root == null) return;// 前序遍历的代码traverse(root.left);traverse(root.right);
}

在递归遍历两棵子树之前写的代码就是前序遍历代码,那么看一看如下伪码:

LinkedList<Integer> res;
void traverse(TreeNode root) {if (root == null) {// 暂且用数字 -1 代表空指针 nullres.addLast(-1);return;}/****** 前序遍历位置 ******/res.addLast(root.val);/***********************/traverse(root.left);traverse(root.right);
}

调用 traverse 函数之后,你是否可以立即想出这个 res 列表中元素的顺序是怎样的?

比如如下二叉树(# 代表空指针 null),可以直观看出前序遍历做的事情:

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

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

// 代表分隔符的字符
String SEP = ",";
// 代表 null 空指针的字符
String NULL = "#";
// 用于拼接字符串
StringBuilder sb = new StringBuilder();/* 将二叉树打平为字符串 */
void traverse(TreeNode root, StringBuilder sb) {if (root == null) {sb.append(NULL).append(SEP);return;}/****** 前序遍历位置 ******/sb.append(root.val).append(SEP);/***********************/traverse(root.left, sb);traverse(root.right, sb);
}

StringBuilder 可以用于高效拼接字符串,所以也可以认为是一个列表,用 , 作为分隔符,用 # 表示空指针 null,调用完 traverse 函数后,StringBuilder 中的字符串应该是 1,2,#,4,#,#,3,#,#,

至此,我们已经可以写出序列化函数 serialize 的代码了:

String SEP = ",";
String NULL = "#";/* 主函数,将二叉树序列化为字符串 */
String serialize(TreeNode root) {StringBuilder sb = new StringBuilder();serialize(root, sb);return sb.toString();
}/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {if (root == null) {sb.append(NULL).append(SEP);return;}/****** 前序遍历位置 ******/sb.append(root.val).append(SEP);/***********************/serialize(root.left, sb);serialize(root.right, sb);
}

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

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

String data = "1,2,#,4,#,#,3,#,#,";
String[] nodes = data.split(",");

这样,nodes 列表就是二叉树的前序遍历结果,问题转化为:如何通过二叉树的前序遍历结果还原一棵二叉树?

PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树。

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

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

/* 主函数,将字符串反序列化为二叉树结构 */
TreeNode deserialize(String data) {// 将字符串转化成列表LinkedList<String> nodes = new LinkedList<>();for (String s : data.split(SEP)) {nodes.addLast(s);}return deserialize(nodes);
}/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<String> nodes) {if (nodes.isEmpty()) return null;/****** 前序遍历位置 ******/// 列表最左侧就是根节点String first = nodes.removeFirst();if (first.equals(NULL)) return null;TreeNode root = new TreeNode(Integer.parseInt(first));/***********************/root.left = deserialize(nodes);root.right = deserialize(nodes);return root;
}

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

三、后序遍历解法

二叉树的后续遍历框架:

void traverse(TreeNode root) {if (root == null) return;traverse(root.left);traverse(root.right);// 后序遍历的代码
}

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

/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {if (root == null) {sb.append(NULL).append(SEP);return;}serialize(root.left, sb);serialize(root.right, sb);/****** 后序遍历位置 ******/sb.append(root.val).append(SEP);/***********************/
}

我们把对 StringBuilder 的拼接操作放到了后续遍历的位置,后序遍历导致结果的顺序发生变化:

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

是不是也简单地将关键代码放到后序遍历的位置就行了呢:

/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<String> nodes) {if (nodes.isEmpty()) return null;root.left = deserialize(nodes);root.right = deserialize(nodes);/****** 后序遍历位置 ******/String first = nodes.removeFirst();if (first.equals(NULL)) return null;TreeNode root = new TreeNode(Integer.parseInt(first));/***********************/return root;
}

没这么简单,显然上述代码是错误的,变量都没声明呢,就开始用了?生搬硬套肯定是行不通的,回想刚才我们前序遍历方法中的 deserialize 方法,第一件事情在做什么?

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

可见,root 的值是列表的最后一个元素。我们应该从后往前取出列表元素,先用最后一个元素构造 root,然后递归调用生成 root 的左右子树。

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

看完整代码:

/* 主函数,将字符串反序列化为二叉树结构 */
TreeNode deserialize(String data) {LinkedList<String> nodes = new LinkedList<>();for (String s : data.split(SEP)) {nodes.addLast(s);}return deserialize(nodes);
}/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<String> nodes) {if (nodes.isEmpty()) return null;// 从后往前取出元素String last = nodes.removeLast();if (last.equals(NULL)) return null;TreeNode root = new TreeNode(Integer.parseInt(last));// 限构造右子树,后构造左子树root.right = deserialize(nodes);root.left = deserialize(nodes);return root;
}

至此,后续遍历实现的序列化、反序列化方法也都实现了。

四、中序遍历解法

先说结论,中序遍历的方式行不通,因为无法实现反序列化方法 deserialize

序列化方法 serialize 依然容易,只要把字符串的拼接操作放到中序遍历的位置就行了:

/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb) {if (root == null) {sb.append(NULL).append(SEP);return;}serialize(root.left, sb);/****** 中序遍历位置 ******/sb.append(root.val).append(SEP);/***********************/serialize(root.right, sb);
}

但是,我们刚才说了,要想实现反序列方法,首先要构造 root 节点。前序遍历得到的 nodes 列表中,第一个元素是 root 节点的值;后序遍历得到的 nodes 列表中,最后一个元素是 root 节点的值。

看上面这段中序遍历的代码,root 的值被夹在两棵子树的中间,也就是在 nodes 列表的中间,我们不知道确切的索引位置,所以无法找到 root 节点,也就无法进行反序列化。

五、层级遍历解法

首先,先写出层级遍历二叉树的代码框架:

void traverse(TreeNode root) {if (root == null) return;// 初始化队列,将 root 加入队列Queue<TreeNode> q = new LinkedList<>();q.offer(root);while (!q.isEmpty()) {TreeNode cur = q.poll();/* 层级遍历代码位置 */System.out.println(root.val);/*****************/if (cur.left != null) {q.offer(cur.left);}if (cur.right != null) {q.offer(cur.right);}}
}

上述代码是标准的二叉树层级遍历框架,从上到下,从左到右打印每一层二叉树节点的值,可以看到,队列 q 中不会存在 null 指针。

不过我们在反序列化的过程中是需要记录空指针 null 的,所以可以把标准的层级遍历框架略作修改:

void traverse(TreeNode root) {if (root == null) return;// 初始化队列,将 root 加入队列Queue<TreeNode> q = new LinkedList<>();q.offer(root);while (!q.isEmpty()) {TreeNode cur = q.poll();/* 层级遍历代码位置 */if (cur == null) continue;System.out.println(root.val);/*****************/q.offer(cur.left);q.offer(cur.right);}
}

这样也可以完成层级遍历,只不过我们把对空指针的检验从「将元素加入队列」的时候改成了「从队列取出元素」的时候。

那么我们完全仿照这个框架即可写出序列化方法:

String SEP = ",";
String NULL = "#";/* 将二叉树序列化为字符串 */
String serialize(TreeNode root) {if (root == null) return "";StringBuilder sb = new StringBuilder();// 初始化队列,将 root 加入队列Queue<TreeNode> q = new LinkedList<>();q.offer(root);while (!q.isEmpty()) {TreeNode cur = q.poll();/* 层级遍历代码位置 */if (cur == null) {sb.append(NULL).append(SEP);continue;}sb.append(cur.val).append(SEP);/*****************/q.offer(cur.left);q.offer(cur.right);}return sb.toString();
}

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

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

/* 将字符串反序列化为二叉树结构 */
TreeNode deserialize(String data) {if (data.isEmpty()) return null;String[] nodesStr = data.split(SEP);// 第一个元素就是 root 的值TreeNode root = new TreeNode(Integer.parseInt(nodesStr[0]));// 队列 q 记录父节点,将 root 加入队列Queue<TreeNode> q = new LinkedList<>();q.offer(root);for (int i = 1; i < nodesStr.length; ) {// 队列中存的都是父节点TreeNode parent = q.poll();// 父节点对应的左侧子节点的值String leftStr = nodesStr[i++];if (!NULL.equals(leftStr)) {TreeNode left = new TreeNode(Integer.parseInt(leftStr));parent.left = left;q.offer(left);} else {parent.left = null;}// 父节点对应的右侧子节点的值String rightStr = nodesStr[i++];if (!NULL.equals(rightStr)) {TreeNode right = new TreeNode(Integer.parseInt(rightStr));parent.right = right;q.offer(right);} else {parent.right = null;}}return root;
}

这段代码可以考验一下你的框架思维。仔细看一看 for 循环部分的代码,发现这不就是标准层级遍历的代码衍生出来的嘛:

while (!q.isEmpty()) {TreeNode cur = q.poll();if (cur.left != null) {q.offer(cur.left);}if (cur.right != null) {q.offer(cur.right);}
}

只不过,标准的层级遍历在操作二叉树节点 TreeNode,而我们的函数在操作 nodesStr[i],这也恰恰是反序列化的目的嘛。

/*
反序列化二叉树:列表->二叉树
核心思想也是BFS:要用一个指针保存现在遍历到的树的位置
不为null就说明是有效节点,要创建出来并构建连接;为null直接跳过
*/
public TreeNode deserialize2(String data) {if (data.isEmpty()) {return null;}// 将data的节点数据转化到String[]里String[] nodesStr = data.split(",");Queue<TreeNode> q = new LinkedList<>();// dataStr[0]就是根节点TreeNode root = new TreeNode(Integer.parseInt(nodesStr[0]));// 根节点入队q.offer(root);// 遍历节点的索引int i = 1;while (!q.isEmpty()) {// 弹出当前节点(也是父节点),该节点用于接下来构建左右子节点的父节点TreeNode node = q.poll();// 判断node的左子节点是否为null,若不为null就构建树并将其加入队列;为null则直接跳过String leftStr = nodesStr[i];if (!NULL.equals(leftStr)) {TreeNode left = new TreeNode(Integer.parseInt(leftStr));node.left = left;q.offer(left);} else {node.left = null;}// 索引移动i++;String rightStr = nodesStr[i];if (!NULL.equals(rightStr)) {TreeNode right = new TreeNode(Integer.parseInt(rightStr));node.right = right;q.offer(right);} else {node.right = null;}i++;}return root;
}

【leetcode】二叉树,297二叉树的序列化与反序列化相关推荐

  1. leetcode:297. 二叉树的序列化与反序列化

    题目来源 leetcode 题目描述 . struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode() : val(0), le ...

  2. leetcode第297——二叉树的序列化与反序列化(层次遍历)

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

  3. 微软面试题Leetcode 428 n叉树的序列化和反序列化

    大致思想是记录每个节点的儿子个数,自定义一个序列化协议 /* // Definition for a Node. class Node { public:int val;vector<Node* ...

  4. 【LeetCode】【HOT】297. 二叉树的序列化与反序列化(BFS)

    [LeetCode][HOT]297. 二叉树的序列化与反序列化 文章目录 [LeetCode][HOT]297. 二叉树的序列化与反序列化 package hot;import java.util. ...

  5. Java实现 LeetCode 297 二叉树的序列化与反序列化

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

  6. LeetCode 297. 二叉树的序列化与反序列化 | Python

    文章目录 297. 二叉树的序列化与反序列化 题目 解题思路 代码实现 实现结果 总结 297. 二叉树的序列化与反序列化 题目来源:力扣(LeetCode)https://leetcode-cn.c ...

  7. LeetCode 297. 二叉树的序列化与反序列化(前序遍历层序遍历)

    文章目录 1. 题目 2. 解题 2.1 前序遍历 2.2 层序遍历 1. 题目 序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过 ...

  8. 【LeetCode 二叉树专项】二叉树的序列化与反序列化(297)

    文章目录 1. 题目 1.1 示例 1.2 说明 1.3 提示 1.4 进阶 2. 解法一(前序遍历) 2.1 分析 2.2 解答 2.3 复杂度 3. 解法二(后序遍历) 3.1 分析 3.2 解答 ...

  9. LeetCode Java刷题笔记—297. 二叉树的序列化与反序列化

    297. 二叉树的序列化与反序列化 请实现两个函数,分别用来序列化和反序列化二叉树.这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化 ...

最新文章

  1. Python 搭建车道智能检测系统
  2. 自动机器学习:团队如何在自动学习项目中一起工作?(附链接)
  3. Redis面试连环问:集群、复制以及与其他NOSQL数据库的区别?
  4. 斯坦福大学报告称中国AI论文引用率首超美国!但李国杰院士也发文灵魂拷问!...
  5. java .split(,)
  6. watir添加新标签
  7. Android之Toast---消息提示
  8. VTK:几何对象之CellTypeSource
  9. fortran_Fortran 60岁生日快乐
  10. main(int argc,char *argv[ ],char *env)参数详解
  11. Hadoop HDFS 的 Java API 操作方式
  12. QQ去除未读状态的动画
  13. SpringBoot添加FastJson消息转换器(自用)
  14. 连点4次android版本,连点器极速版下载-连点器快速版v4.0.8 安卓版 - 极光下载站...
  15. 几款好用的敏捷开发工具
  16. 打印机十大共性故障解决方法!!!
  17. 微信隐藏功能:微信怎么群发消息给好友不建群?
  18. 【C/C++】输入一个整数的二目运算式的字符串,如100+20,332-19,200*2333,44/33二目运算取”加减乘除“中的一种输出运算式的整数结果值
  19. Synthesys:语音合成和视频生成平台
  20. LazyBrush论文笔记(4):问题建模-平滑项与数据项

热门文章

  1. 计算机屏幕无信号咋回事,电脑显示屏无信号怎么回事?电脑打不开显示器无信号的解决办法...
  2. C语言关键字你知道几个?
  3. 软件工程中常见的工具介绍
  4. 机器学习-定序回归及python实现
  5. 附近的人mysql实现_附近的人功能实现及原理
  6. 基于28335实现的旋变软解码 利用三角函数积化和差公式将旋变输出信号分解为高频和低频两部分… 锁相环
  7. Cisco路由器配置手册
  8. Mysql 事务(标贝科技)
  9. 马克笔的笔法和技巧(入门基础)
  10. 微信小程序怎么嵌入手机版的网页?