[[树 字符串] 297. 二叉树的序列化与反序列化(DFS、BFS)
297. 二叉树的序列化与反序列化
题目链接:https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/
分类:
- 树、字符串(二叉树序列化:树 → 字符串,二叉树反序列化:字符串 → 树);
- DFS:(思路2)
- 序列化:先序遍历得到序列 + 空节点也加入序列;
- 反序列化:全局遍历序列(全局变量 或 队列) + 前序遍历构建二叉树 ;
- BFS:(思路1)
- 序列化:层次遍历得到序列 + 空节点也加入序列;
- 反序列化:全局遍历序列 + 层次遍历构建二叉树;
- 集合知识:数组转存到队列(数组 -(Arrays.asList)-> 列表 -(构造器)->队列)
思路1:BFS
序列化:对二叉树做BFS即做层次遍历,遇到非空节点就向序列插入节点值,遇到空节点时就向序列插入"null",节点之间也要用","隔开,整个序列的首尾分别是 “[” 和 “]” 。
算法设计:
-
设置一个队列对二叉树做层次遍历,需要注意的是如果节点为空节点也要入队。再开辟一个StringBuilder sb来存放序列化过程中的序列,初始时先向sb尾部插入 “[”;
-
层次遍历按常规方法实现,每出队一个元素,判断它是不是null:
如果不是null节点,就将其左右孩子入队(就算左右孩子为空节点也入队),再将节点的val加入sb末尾,再加上逗号;
如果是null节点,就不再处理它的左右孩子,只将"null"插入sb末尾,再加上逗号。
注意:为了简化代码,每个元素输出到字符串上时统一在后面加上逗号,在得到最终的字符串时再将最末尾的逗号去掉,换成 “]”,就能得到符合题目要求的序列。
-
并不是所有空节点都要输出"null",例如示例中4和5的孩子节点都为null,但不需要再输出这些null了,体现在层次遍历过程中就是:如果此时队内所有元素都是空节点,就不再输出到sb上。
这样的设置在一些边界情况如:root==null,会输出"[]",所以我们需要一些细节上的调整,见代码注释。
由此带来一个问题:如何判断当前队列内是不是全为空节点?
方法:设置一个计数器count,在遍历过程中记录队列内的空节点个数,每入队一个空节点count+1,每出队一个空节点count-1。如果count == queue.size(),说明队内全是空节点。
- 注意:queue.size()返回队内元素个数,元素null也计算在内。
反序列化:使用BFS将序列化生成的字符串再转换为二叉树。
算法设计:
-
将序列化得到的字符串去除首尾的"[“和”]",再按逗号分割,得到字符串数组nodes;设置一个队列用于对已生成的二叉树做BFS;
-
先取字符串数组nodes[0]生成根节点,然后将根节点入队,设置一个指针idx作为nodes数组上的工作指针,初始时idx=1;
-
BFS的过程其实是边创建节点边做BFS,每一轮就拿此时nodes数组上的nodes[idx]创建节点,然后将其作为当前节点的孩子节点。先确定当前层的节点个数(=此时的队列长度),然后将当前层的节点依次从队列中弹出,每弹出一个节点top,此时nodes数组上nodes[idx]和nodes[idx+1]分别是它的左右孩子节点:
以左孩子为例:
– 如果nodes[idx]不为"null",就将字符串元素转换成数字创建对应的TreeNode,作为top的左孩子,然后将top.left加入队列;
– 如果nodes[idx]为"null",就直接跳过;
左孩子处理完后idx+1;
右孩子同理,在处理完毕后,同样需要再次将idx+1;
- 注意:无论当前nodes[idx]是否为"null",每遍历完一个元素,idx+1。(易遗漏)
直到队列为空,BFS结束。
实现代码:
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
//特殊用例
//if(root == null) return "[null]";
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//sb存放生成的序列
StringBuilder sb = new StringBuilder();
//先向sb加入"["
sb.append("[");
int count = 0;//记录队内空节点个数(不包括根节点)
while(!queue.isEmpty()){
int size = queue.size();
//如果队内元素都是空节点,则直接退出(根节点为空的情况不会退出)
if(size == count) break;
for(int i = 0; i < size; i++){
TreeNode top = queue.poll();
//如果出队的节点不为空,则取节点值加入sb末尾(无论是不是最后一个节点后面都加上逗号,在收尾时再删除)
if(top != null){
sb.append(top.val).append(",");
//左右孩子入队
queue.offer(top.left);
queue.offer(top.right);
//如果入队的是空节点,则count增加
if(top.left == null) count++;
if(top.right == null) count++;
}
//如果出队的节点为空,则将"null"加入sb末尾,不需要再处理它的左右孩子
else{
sb.append("null").append(",");
count--;
}
}
}
//将sb末尾的逗号删除
sb.setLength(sb.length() - 1);
//向sb末尾加入"]"后再返回
return sb.append("]").toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
//特殊用例
if(data.equals("[null]")) return null;
//为了便于处理将data首尾中括号去除,然后按逗号分割成字符串数组
String[] nodes = data.substring(1, data.length() - 1).split(",");
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
queue.offer(root);
int idx = 1;//nodes上的工作指针
while(!queue.isEmpty()){
int size = queue.size();//二叉树当前层的节点个数
for(int i = 0; i < size; i++){
TreeNode top = queue.poll();
//添加左孩子:字符串元素不为null才添加为左孩子,否则直接跳过
if(idx < nodes.length && !nodes[idx].equals("null")){
top.left = new TreeNode(Integer.parseInt(nodes[idx]));
queue.offer(top.left);
}
idx++;//无论字符串是否为"null",工作指针都要+1
//添加右孩子:字符串元素不为null才添加为右孩子,否则直接跳过
if(idx < nodes.length && !nodes[idx].equals("null")){
top.right = new TreeNode(Integer.parseInt(nodes[idx]));
queue.offer(top.right);
}
idx++;
}
}
return root;
}
}
思路2:DFS
看题解发现首尾两个中括号可以不用加。。。所以思路2的序列不考虑中括号,递归代码更好写。
序列化:
这里使用的是前序遍历,和常规的前序遍历基本相同,不同点在于这里需要将空节点也作为"null"加入遍历序列,元素之间要加逗号隔开。
例如:
1
/ \
2 3
/ \
4 5
得到的前序遍历序列是:1,2,null,null,3,4,null,null,5,null,null
反序列化:
根据上面序列化得到的前序遍历序列data生成对应的二叉树。我们按次序遍历序列,对每个遍历到的元素按前序遍历的方式构建节点和它的左右孩子。
例如:1,2,null,null,3,4,null,null,5,null,null
访问nodes[0]=1,创建一个节点root,root.val=1;
- 继续调用递归访问nodes[1]=2,返回val=2的节点作为root的左孩子;
- 左子树的构建在更深层的递归中自动完成,在返回节点给root时左子树已经构建完成,右子树同理。
- 继续调用递归访问nodes[2]=null,返回空节点作为root的右孩子。
注意:对nodes的遍历是全局性的,整个递归过程共享它的遍历状态。
所以问题就在于:如何全局遍历nodes数组?
- 方法1:设置全局变量idx作为遍历nodes的工作指针;
- 方法2:将nodes转存为队列,每次弹出一个节点。
算法流程:
(方法1)
1、将序列data按逗号分割,得到字符串数组nodes,设置一个全局变量idx作为nodes上的工作指针,初始值=1;
2、进入DFS:
- 如果nodes[idx]==“null”,返回null;
- 如果nodes[idx]!=“null”,则将nodes[idx]转成数字,作为val创建一个节点root;
- idx++,继续调用递归,返回值作为root的左孩子;
- idx++,继续调用递归,返回值作为root的右孩子。
- 返回root。
(方法2)
整体流程和方法1大致相同,但需要先将nodes数组转换成队列,在前序遍历构造二叉树的过程中,获取nodes元素时是从队列中弹出队首,不需要idx指针。
知识点:数组转成队列(数组 -(Arrays.asList)-> 列表 -(构造器)->队列)
数组先利用Arrays.asList转换成列表,再利用队列的构造器转存为队列:
Queue<String> queue = new LinkedList<>(Arrays.asList(nodes));
- 注意:Arrays.asList转换得到的列表长度是不可变的,也没有add,remove方法,同时建议只对引用数组使用,对基本数据类型的数组使用会出错。
实现代码:
//思路2:DFS,先序遍历
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) return "null";
String left = serialize(root.left);
String right = serialize(root.right);
return root.val + "," + left + "," + right;//序列不考虑首尾的中括号
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
//特殊用例
if(data.equals("[null]")) return null;
//为了便于处理将data按逗号分割成字符串数组
String[] nodes = data.split(",");
return helper1(nodes);
}
int idx = 0;//用于全局遍历nodes数组的工作指针
//反序列化:前序遍历 + 方法1
private TreeNode helper1(String[] nodes){
if(nodes[idx].equals("null")) return null;
TreeNode root = new TreeNode(Integer.parseInt(nodes[idx]));
idx++;
root.left = helper1(nodes);
idx++;
root.right = helper1(nodes);
return root;
}
//反序列化:前序遍历 + 方法2
public TreeNode helper2(Queue<String> queue){
String node = queue.poll();
if(node.equals("null")) return null;
TreeNode root = new TreeNode(Integer.parseInt(node));
root.left = helper2(queue);
root.right = helper2(queue);
return root;
}
}