本文参考labuladong算法笔记[二叉树心法(序列化篇) | labuladong 的算法笔记]
要说序列化和反序列化,得先从 JSON 数据格式说起。
JSON 的运用非常广泛,比如我们经常将编程语言中的结构体序列化成 JSON 字符串,存入缓存或者通过网络发送给远端服务,消费者接受 JSON 字符串然后进行反序列化,就可以得到原始数据了。
这就是序列化和反序列化的目的,以某种特定格式组织数据,使得数据可以独立于编程语言。
那么假设现在有一棵用 Java 实现的二叉树,我想把它通过某些方式存储下来,然后用 C++ 读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行序列化和反序列化了。
概述:前/中/后序和二叉树的唯一性
谈具体的题目之前,我们先思考一个问题:什么样的序列化的数据可以反序列化出唯一的一棵二叉树?
比如说,如果给你一棵二叉树的前序遍历结果,你是否能够根据这个结果还原出这棵二叉树呢?
答案是也许可以,也许不可以,具体要看你给的前序遍历结果是否包含空指针的信息。如果包含了空指针,那么就可以唯一确定一棵二叉树,否则就不行。
举例来说,如果我给你这样一个不包含空指针的前序遍历结果 [1,2,3,4,5]
,那么如下两棵二叉树都是满足这个前序遍历结果的:
所以给定不包含空指针信息的前序遍历结果,是不能还原出唯一的一棵二叉树的。
正确答案是,即便你包含了空指针的信息,也只有前序和后序的遍历结果才能唯一还原二叉树,中序遍历结果做不到。
更直观的,比如如下两棵二叉树显然拥有不同的结构,但它俩的中序遍历结果都是 [#,1,#,1,#]
,无法区分:
核心区别
总结下结论,当二叉树中节点的值不存在重复时:
-
如果你的序列化结果中不包含空指针的信息,且你只给出一种遍历顺序,那么你无法还原出唯一的一棵二叉树。
-
如果你的序列化结果中不包含空指针的信息,且你会给出两种遍历顺序,那么按照前文 东哥手把手带你刷二叉树(构造篇) 所说,分两种情况:
2.1. 如果你给出的是前序和中序,或者后序和中序,那么你可以还原出唯一的一棵二叉树。
2.2. 如果你给出前序和后序,那么你无法还原出唯一的一棵二叉树。
-
如果你的序列化结果中包含空指针的信息,且你只给出一种遍历顺序,也要分两种情况:
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]总是指向该节点的子节点,妙!