剑指Offer_#37 序列化二叉树
Contents
题目
请实现两个函数,分别用来序列化和反序列化二叉树。
示例:
你可以将以下二叉树:
1
/
2 3
/
4 5
序列化为 "[1,2,3,null,null,4,5]"
思路分析
题意分析
-
用哪种遍历方式进行序列化?
这一题给出的示例,序列化时采用层序遍历(BFS),与书上不一样,书中是用前序遍历。但是实际上根据代码模板里边的说明:Your Codec object will be instantiated and called as such: Codec codec = new Codec(); codec.deserialize(codec.serialize(root));
在Leetcode平台中,它测试的方法是:对一个二叉树先做序列化,再做反序列化,然后看结果是否跟输入的二叉树相同。
而没有去分别测试序列化的结果,反序列化的结果。
所以说,要通过这一题,无论用哪种遍历方式都是可以的,重点在于,serialize
和deserialize
函数是互逆的。
看到题目的例子,我下意识认为序列化必须是层序遍历,感觉具有一定的迷惑性...层序遍历也是写法之一,但是代码较为复杂,也可以选择使用代码较简单的前序遍历。 -
与剑指 Offer 07. 重建二叉树的区别?
- 7题是通过前序遍历和中序遍历两个序列建立二叉树,而本题只需要用一种序列就可以重建二叉树。
- 原因是:7题中给出的序列不包含
null
节点,一个序列中没有足够的信息量来重建二叉树,必须依赖两个序列重建。
思路
解法1:层序遍历(BFS)
-
序列化
参考剑指Offer_#32_从上到下打印二叉树的写法,改动的地方是:- 函数返回值变为
String
,使用StringBuilder
来迭代式的构造字符串。 while
循环中,可以向队列中加入是null
的子节点;同时也要针对节点为null
的情况做特殊处理,用一个“null”
字符串来表示。
- 函数返回值变为
-
反序列化
依然要借助队列来辅助。整体思路如下:- 队列存储的是待构造的节点(
val
已经设置,但左右指针还未设置好),每次取出头部节点,设置它的left
和right
指针。 - 同时遍历序列化字符串和二叉树节点队列
- 每次从队列取出一个节点
node
,对应的从序列化字符串里取出它的左右子节点,连接到node
上。 - 然后将左右子节点加入队列(因为他们的左右指针还未设置好)
- 每次从队列取出一个节点
- 一个关键点:对于
“null”
的处理- 序列化字符串中遇到
null
的时候,不需要做任何处理。 - 因为:
null
节点不需要被连接到node
,因为初始化时,node
的左右子节点默认就是null
;null
节点不需要设置左右子节点,所以不需要加入队列。
- 序列化字符串中遇到
- 队列存储的是待构造的节点(
解法2:前序遍历
- 序列化
树的前序遍历。唯一特殊的地方是,需要将结果转换成String
再返回。 - 反序列化
递归遍历vals
和树。- 出口条件: 遍历到
$
,返回null
- 递推工作
- 构建一个节点
node
,移动指向vals
元素的指针index
- 构建当前节点的左右子节点
- 返回
node
- 构建一个节点
- 出口条件: 遍历到
解答
解答1:层序遍历
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("[");
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode node = q.poll();
if(node != null){
res.append(node.val + ",");
q.offer(node.left);
q.offer(node.right);
}
//如果当前访问的节点是null,那么就不需要添加其左右子节点到队列
else
res.append("null,");
}
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1,data.length() - 1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
//由于第索引为0的是根节点,已经入队列,所以while循环当中是从索引1开始的
int i = 1;
while(!q.isEmpty()){
TreeNode node = q.poll();
//构建左子节点
//如果左子节点对应的值是“null”,则不做任何处理,直接跳过
if(!vals[i].equals("null")){
TreeNode leftNode = new TreeNode(Integer.parseInt(vals[i]));
node.left = leftNode;
q.offer(leftNode);
}
i++;
//构建右子节点
if(!vals[i].equals("null")){
TreeNode rightNode = new TreeNode(Integer.parseInt(vals[i]));
node.right = rightNode;
q.offer(rightNode);
}
i++;
}
return root;
}
}
解答2:前序遍历
public class Codec {
String[] vals;
int index = 0;//vals[]的索引
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder();
res.append("[");
serializeRecur(root,res);
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
void serializeRecur(TreeNode root,StringBuilder res){
if(root == null){
res.append("$,");
return;
}
//形参res指向的内存空间不变,所以可以修改原本的res变量
res.append(root.val + ",");
serializeRecur(root.left,res);
serializeRecur(root.right,res);
return;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1,data.length() - 1).split(",");
this.vals = vals;
return deserializeRecur();
}
TreeNode deserializeRecur(){
if(vals[index].equals("$")){
index++;
return null;
}
else{
//构建完当前的节点,就增加index,指向下一个节点的值
TreeNode node =new TreeNode(Integer.parseInt(vals[index++]));
node.left = deserializeRecur();
node.right = deserializeRecur();
return node;
}
}
}
Java基础:值传递和引用传递
上面的两个递归函数serializeRecur
和deserializeRecur
当中体现了Java参数传递的知识。
deserializeRecur
的一种错误写法
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1,data.length() - 1).split(",");
this.vals = vals;
TreeNode root = new TreeNode();
deserializeRecur(root);
return root;
}
void deserializeRecur(TreeNode node){
if(vals[index].equals("$")){
node = null;
index++;
return;
}
else{
//错误就在这句,这里把node形参指向了一个新对象,那么之后的所有操作,都和调用处传入的root无关了,root不会被改变
node = new TreeNode(Integer.parseInt(vals[index++]));
deserializeRecur(node.left);
deserializeRecur(node.right);
}
}
这是我第一次照猫画虎照着书上的C++代码写下来的, 但是Java跟C++不同啊,C++代码是:
void Deserialize(BinaryTreeNode** pRoot, istream& stream)
{
int number;
if(ReadStream(stream, &number))
{
*pRoot = new BinaryTreeNode();
(*pRoot)->m_nValue = number;
(*pRoot)->m_pLeft = nullptr;
(*pRoot)->m_pRight = nullptr;
Deserialize(&((*pRoot)->m_pLeft), stream);
Deserialize(&((*pRoot)->m_pRight), stream);
}
}
C++当中,pRoot
是一个指针,*pRoot
是pRoot
指向的内存空间,这里用赋值语句,修改的就是指针指向的内容;
但是在java当中,node
并不等同于指针,赋值语句不会修改node
原本指向的内存空间,而是把node
指向一个新创建的TreeNode
对象的内存空间。
关于这部分知识,可以参考