题目
链接:https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/
问题描述:
解析
方法1:DFS(递归)
递归可以理解为:转交职责
- 递归一棵树,只关注当前的单个节点就好
- 剩下的工作,交给递归完成
- “serialize 函数,你能不能帮我序列化我的左右子树?我等你的返回结果,再追加到我身上。”
- 为什么选择 前序遍历,因为在反序列化时,根|左|右,更容易定位根节点值
- 遇到 null 节点也要翻译成一个特殊符号,反序列化时才知道这里对应 null 节点
序列化 代码
const serialize = (root) => {
if (root == null) return 'X,' // 遇到null节点
const leftSerialized = serialize(root.left) //左子树的序列化字符串
const rightSerialized = serialize(root.right) //右子树的序列化字符串
return root.val + ',' + leftSerialized + rightSerialized // 根|左|右
}
反序列化——也是递归
序列化时是前序遍历,所以序列化字符串呈现这样的排列:
“根|(根|(根|左|右)|(根|左|右))|(根|(根|左|右)|(根|左|右))”
构建树的函数 buildTree
- buildTree 接收的 “状态” 是 list 数组,由序列化字符串转成
- 按照前序遍历的顺序:先构建根节点,再构建左子树,再构建右子树
- list 数组首项是 当前子树的根节点,弹出,先构建它
buildTree 关注当前节点,然后职责转交
- 将 list 数组的首项弹出,考察它
- 如果它为 ‘X’ ,直接返回 null ,没有子树可构建
- 如果它不为 ‘X’,则为它创建 node 节点,并构建子树
- 递归调用 buildTree 构建左子树
- 递归调用 buildTree 构建右子树
- 以 node 为根节点的子树,构建完毕,向上返回
反序列化 代码
const buildTree = (list) => { // dfs函数
const nodeVal = list.shift() // 当前考察的节点
if (nodeVal == 'X') return null // 是X,返回null给父调用
const node = new TreeNode(nodeVal) // 创建node节点
node.left = buildTree(list) // 构建node的左子树
node.right = buildTree(list) // 构建node的右子树
return node // 返回以node为根节点的子树给父调用
}
const deserialize = (data) => {
const list = data.split(',') // 转成list数组
return buildTree(list) // 构建树,dfs的入口
}
完整实现:
实现1:耗时比较大
public class Codec {
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode rdeserialize(List<String> l) {
if (l.get(0).equals("None")) {
l.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
l.remove(0);
root.left = rdeserialize(l);
root.right = rdeserialize(l);
return root;
}
public TreeNode deserialize(String data) {
String[] data_array = data.split(",");
List<String> data_list = new LinkedList<String>(Arrays.asList(data_array));
return rdeserialize(data_list);
}
};
实现2:采用StringBuilder类,耗时小
public class Codec {
public String serialize(TreeNode root) { //用StringBuilder
StringBuilder res = ser_help(root, new StringBuilder());
return res.toString();
}
public StringBuilder ser_help(TreeNode root, StringBuilder str){
if(null == root){
str.append("null,");
return str;
}
str.append(root.val);
str.append(",");
str = ser_help(root.left, str);
str = ser_help(root.right, str);
return str;
}
public TreeNode deserialize(String data) {
String[] str_word = data.split(",");
List<String> list_word = new LinkedList<String>(Arrays.asList(str_word));
return deser_help(list_word);
}
public TreeNode deser_help(List<String> li){
if(li.get(0).equals("null")){
li.remove(0);
return null;
}
TreeNode res = new TreeNode(Integer.valueOf(li.get(0)));
li.remove(0);
res.left = deser_help(li);
res.right = deser_help(li);
return res;
}
}
方法2:BFS
序列化 —— 标准的 BFS
- 我让 null 也入列,说它是真实节点也行,它有对应的"X",只是没有子节点入列
考察出列节点 - 如果不为 null ,则将它的值推入 res 数组,并将它的左右子节点入列
- 如果是 null ,则将 ‘X’ 推入 res 数组
- 出列…入列…直到队列为空,所有节点遍历完,res 数组也构建完,转成字符串
序列化 代码
const serialize = (root) => {
const queue = [root]
let res = []
while (queue.length) {
const node = queue.shift()
if (node) { // 出列的节点 带出子节点入列
res.push(node.val)
queue.push(node.left) // 不管是不是null节点都入列
queue.push(node.right)
} else {
res.push('X')
}
}
return res.join(',')
}
反序列化——也是BFS:父节点出列,子节点入列
下图是BFS得到的序列化字符串:
- 除了第一个 ROOT 值,其他节点值都是成对的,分别对应左右子节点
- 我们从第二项开始遍历,每次考察两个节点值
- 先构建的节点,是之后构建的节点的父亲,用一个 queue 暂存一下
- queue 初始放入 ROOT 。父节点出列,找出子节点入列
同时考察父节点,和两个子节点值
-
出列的父节点,它对应到指针指向的左子节点值,和指针右边的右子节点值
-
如果子节点值不为 ‘X’ ,则为它创建节点,并认父亲,并作为未来父亲入列
-
如果子节点值为 ‘X’,什么都不做(父节点本来就有 null 子节点)
-
所有父节点(真实节点)都会在 queue 里走一次
反序列化代码:
const deserialize = (data) => {
if (data == 'X') return null // 就一个'X',只有一个null
const list = data.split(',') // 序列化字符串转成list数组
const root = new TreeNode(list[0]) //首项是根节点值,为它创建节点
const queue = [root] // 初始放入root,待会出列考察
let cursor = 1 // 从list第二项开始遍历
while (cursor < list.length) { // 指针越界就退出
const node = queue.shift() // 父节点出列考察
const leftVal = list[cursor] // 获取左子节点值
const rightVal = list[cursor + 1] // 获取右子节点值
if (leftVal !== 'X') { // 左子节点值是有效值
const leftNode = new TreeNode(leftVal) // 创建节点
node.left = leftNode // 成为当前出列节点的左子节点
queue.push(leftNode) // 它是未来的爸爸,入列等待考察
}
if (rightVal !== 'X') { // 右子节点值是有效值
const rightNode = new TreeNode(rightVal) // 创建节点
node.right = rightNode // 成为当前出列节点的右子节点
queue.push(rightNode) // 它是未来的爸爸,入列等待考察
}
cursor += 2 // 指针前进2位
}
return root // 返回根节点
}
完整实现:
serialize() 序列化
分析
- 使用 前序遍历 每个节点,如果不为null,则将值放入字符串中,如果为null,则放入"null"
- 题目有说明不让用类成员、变量,所以不能用递归处理遍历
- 由于前序遍历正好符合先进先出的原则,所以我们考虑用队列实现前序遍历
- 将当前节点加入队列,判断节点是否为空,若不为空则当前节点出队列并将值拼接到字符串后,然后将当前节点的左右子节点入队列
- 若当前节点为空,则没有子节点,直接拼接"null"字符串
- 重复4、5步骤,直到队列为空
- 对字符串进行处理后将其返回
deserialize() 反序列化
分析
- 将字符串截取并分割,返回字符串数组,数组中的每一项都是一个节点值
- 写一个方法,传节点值,返回该节点值的节点,如果值为"null",则返回null
- 此时我们可以将数组中的每一项看成一个节点
- 问题变成了如何将数组中的每一个节点按照原先的顺序连接起来
- 思路依然是用队列存储节点
- 遍历数组,如果某一个父节点的左右子节点都已找到,那我们就寻找下一个父节点
- 所以我们的队列应该存储尚未找到子节点的父节点
- 那如何判断左右子节点都已找到呢?
1.我们可以用一个变量 isLeft 来存储是否为左节点
2.isLeft默认为true
3.让父节点连接左节点,然后对isLeft取反,此时isLeft为false
4.然后连接右节点,再将isLeft取反,此时isLeft为true
5.如果isLeft又变回true,说明找到了左右子节点
6.此时父节点应该变为队列的队首
当数组遍历完成之后,返回数组第一个节点
完成代码如下:
public String serialize(TreeNode root) {
StringBuilder res = new StringBuilder("["); // 拼接的字符串
Queue<TreeNode> queue = new LinkedList(); // 保存节点队列
queue.add(root); // 先从根节点开始
while (!queue.isEmpty()) {
TreeNode cur = queue.remove(); // 出队首节点
if (cur == null) {
/*
如果节点为空,说明没有子节点,直接拼接null到字符串中
*/
res.append("null,");
} else {
/*
节点不为空,则将值加入字符串中
并且将其左右节点加入队列中
*/
res.append(cur.val + ",");
queue.add(cur.left);
queue.add(cur.right);
}
}
res.setLength(res.length() - 1); // 此时res中的值最后会多一个逗号,所以需要去掉最后一个字符
res.append("]");
return res.toString();
}
public TreeNode deserialize(String data) {
// 将字符串截取并分割,返回字符串数组,数组中的每一项都是一个节点值
String[] arr = data.substring(1, data.length() - 1).split(",");
Queue<TreeNode> queue = new LinkedList<>(); // 存储尚未找到子节点的父节点
TreeNode root = getNode(arr[0]); // 保存数组第一个节点,最终返回它
TreeNode parent = root; // 临时的父节点变量
boolean isLeft = true; // 判断是否已经找到了左右子节点
for (int i = 1; i < arr.length; i++) {
TreeNode cur = getNode(arr[i]); // 遍历数组
if (isLeft) {
// 说明cur是parent的左节点
parent.left = cur;
} else {
// 说明cur是parent的右节点
parent.right = cur;
}
if (cur != null) {
// 说明cur是一个父节点,但其左右子节点可能为空
queue.add(cur);
}
isLeft = !isLeft; // 取反
if (isLeft) {
/*
如果此时为真,说明上面的cur是右节点,左右节点已经找到
队首出列,保存为新的父节点
*/
parent = queue.poll();
}
}
return root;
}
/**
* 返回以val为值的节点
* @param val 如果val为"null",则返回null
* @return 返回以val为值的节点
*/
private TreeNode getNode(String val) {
if (val.equals("null")) {
return null;
}
return new TreeNode(Integer.valueOf(val));
}