描述
请实现两个函数,分别用来序列化和反序列化二叉树,不对序列化之后的字符串进行约束,但要求能够根据序列化之后的字符串重新构造出一棵与原二叉树相同的树。
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树等遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
例如,可以根据层序遍历并特定标志空结点的方案序列化,也可以根据满二叉树结点位置的标号规律来序列化,还可以根据先序遍历和中序遍历的结果来序列化。
假如一棵树共有 2 个结点, 其根结点为 1 ,根结点右子结点为 2 ,没有其他结点。按照上面第一种说法可以序列化为“1,#,2,#,#”,按照上面第二种说法可以序列化为“{0:1,2:2}”,按照上面第三种说法可以序列化为“1,2;2,1”,这三种序列化的结果都包含足以构建一棵与原二叉树完全相同的二叉树的信息。
不对序列化之后的字符串进行约束,所以欢迎各种奇思妙想。
/* TreeNode类的代码
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
示例
input:
{8,6,10,5,7,9,11}
output:
{8,6,10,5,7,9,11}
思路
根据题目的要求,需要将二叉树序列化为字符串并且能够还原即可,本题中的要点是对树进行还原。根据题干的要求,主要有3种方法序列化为字符串:队列的层次遍历、递归的先序或后序遍历、先序+中序遍历。
1、队列遍历:层次遍历
二叉树的层次遍历的思路是:由根节点开始,从上往下一层层的遍历;在每一层中,按照从左到右的顺序遍历。根据这种遍历的特点,一般使用队列。
(1)队列实现二叉树的层次遍历
队列的特性是先进先出,因此使用队列来层次遍历时,需要:
- 先加入根节点,确保队列不为空
- 队列不为空时,取出队列的元素,依次加入该结点的左右子树
- 重复上一步,直到队列为空
由于队列遍历二叉树时,每次循环字符串中依次加入左右两个子树的结点(为空则用#
替代),因此最后的字符串中,每一个根结点必然有左右子结点(叶子结点的左右结点依次为#
、#
)。基于这个特性,根据字符串重构二叉树时,可以使用队列来反序列化:
- 将序列化的字符串按照
,
切割为字符串数组,数组中每个字符串表示一个结点;队列中首先加入根结点 - 由于序列化时每次加入左右子树,因此反序列化时,也要依次解出左右子树(遇到
#
时,直接进入下一个字符串);与此同时,不为空的结点需要加入到队列中 - 重复上一步,直到队列为空或者数组索引越限
可以看到的是,队列在整个序列化和反序列化中,可以筛选叶子结点
- 在序列化中,队列只会加入不为空的结点,防止
null
加入 - 在反序列化中,队列通过阻断叶子结点
#
,从而让字符串中的索引刚好对上队列poll
结点的左右子树
import java.util.*;
public class Solution {
String Serialize(TreeNode root) {
if(root == null) return null;
Queue<TreeNode> que = new LinkedList<>();
//队列先加入根结点,字符串加入根结点的值,以','分割
que.offer(root);
StringBuilder sb = new StringBuilder(root.val + ",");
TreeNode tmp = null;
while(!que.isEmpty()){
//取出当前结点之后,字符串中依次加入左右子树的结点
tmp = que.poll();
if(tmp.left != null){
sb.append(tmp.left.val + ",");
que.offer(tmp.left);
}else sb.append("#,");
if(tmp.right != null){
sb.append(tmp.right.val + ",");
que.offer(tmp.right);
}else sb.append("#,");
}
return sb.toString();
}
TreeNode Deserialize(String str) {
if(str == null) return null;
String[] e = str.split(",");
Queue<TreeNode> que = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(e[0]));
TreeNode tmp = null;
que.offer(root);//加入根结点
int idx = 1;
while(!que.isEmpty() && idx < e.length){
tmp = que.poll();
if(e[idx].charAt(0) != '#'){
tmp.left = new TreeNode(Integer.parseInt(e[idx]));
que.offer(tmp.left);
}
idx++;
if(idx < e.length && e[idx].charAt(0) != '#'){
tmp.right = new TreeNode(Integer.parseInt(e[idx]));
que.offer(tmp.right);
}
idx++;
}
return root;
}
}
注意:由于Queue
不能加入null
元素,因此队列的while
循环不能这样写:
que.offer(root);
StringBuilder sb = new StringBuilder();
TreeNode tmp = null;
while(!que.isEmpty()){
tmp = que.poll();
sb.append(tmp == null ? "#," : tmp.val + ",");
que.offer(tmp.left);
que.offer(tmp.right);
}
2、递归版遍历:先序、后序
由于本题中要求根据序列化的结果来反序列化出二叉树,因此,在序列化的结果中,叶子结点可以使用#
代替,因此每一个结点都含有左右子树(没有子树的用#
代替,如下图中2
的右子树为空,但序列化中用#
表示其右子树)。这么做的好处是:在反序列化时,确保每一个结点都有左右子树(字符为#
表示子树为null
,否则表示有真实的子树),这样的写法非常适合递归来实现。
在先序和后续遍历中,根结点的位置可以直接确定,因此这两种遍历中任何一个都可以直接反序列化出二叉树;而中序遍历的序列化结果中,由于无法确定二叉树根结点的位置,因此只依靠中序遍历无法反序列化出二叉树。在后面将会用结合中序和先序、中序和后续来反序列化二叉树。
(1)递归先序遍历
二叉树的先序遍历的思路是:先访问根结点,然后先序遍历左子树,最后先序遍历右子树。在使用递归实现时,需要先访问根结点,如果还有左子树,那么需要访问左子树的根结点… …如此递归直到最左边的叶子结点(如下图中的结点4
),此时该叶子结点对应的父结点如果有右子树,则先序访问右子树。下图中的先序遍历的顺序为:1-2-4-3-5
,下面代码中序列化的字符串结果为:1,2,4,#,#,#,3,#,5,#,#,