leetcode297 二叉树的序列化与反序列化
20201020更新
引用b站扶我起来继续study
主要思路:
序列化:把一棵二叉树序列化为一个字符串序列:按照先序遍历dfs序列化每一个节点,空节点的情况,非空节点的情况
反序列化:
通过序列化的序列,按照先序遍历构建一颗二叉树:
node->val=num; // 需要一个idx定位数字符号,len定位’,’
node->left=dfs2();
node->right=def2();
return node;
代码如下:
// Encodes a tree to a single string.
string serialize(TreeNode *root)
{
string res;
// 如果要返回字符,那么:
// char* p=new char[res.size()+1];
// strcpy(p,res.c_str());
dfs1(root, res);
// return p;
return res;
}
void dfs1(TreeNode *root, string &res)
{
// 空节点
if (!root)
{
//#
res += "#,";
return;
}
// 序列化采用的是先序遍历
// 非空节点
res += to_string(root->val) + ",";
// 递归
dfs1(root->left, res);
dfs1(root->right, res);
}
// Decodes your encoded data to tree.
// 反序列化就是传入一个字符数组构建一颗二叉树
TreeNode *deserialize(string data)
{
int idx = 0; // 序列化后为:1, 2, #, #, 3, 4, #, #, 5, #, #, idx指向1的位置
return dfs2(data, idx);
}
TreeNode *dfs2(string data, int &idx)
{
// 确定长度
int len = idx;
while (data[len] != ',')
len++; // idx指向数字1,len指向数字1后的","
// 然后再用idx与len来解析字符串str
// 如果空节点
if (data[idx] == '#')
{
idx = len + 1; // 指向下一个数字字符
return NULL;
}
// 非空节点
// 计算数字字符的数值
int num = 0;
// 考虑符号正负
int sign = 1;
if (idx < len && data[idx] == '-')
sign = -1, idx++; // idx指向-后的数字
for (int i = idx; i < len; i++)
num = num * 10 + data[i] - '0';
num *= sign;
// idx走到下一个数字字符
idx = len + 1;
// 构建二叉树
auto root = new TreeNode(num);
root->left = dfs2(data, idx);
root->right = dfs2(data, idx);
return root;
}
方法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,接收的是由序列化字符串转成 list 数组
-
按照前序遍历的顺序构建:先构建根节点,再构建左子树、右子树
-
将 list 数组的首项弹出,它是当前子树的 root 节点
- 如果它为 ‘X’ ,返回 null
- 如果它不为 ‘X’,则为它创建节点,并递归调用 buildTree 构建左右子树,构建完当前子树构建,向上返回
-
反序列化代码
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;
}
const deserialize = (data) => {
const list = data.split(',') // 转成list数组
return buildTree(list) // 构建树,dfs的入口
}
- 按照大佬思路翻译的DFS C++版本
class Solution{
public:
// Encodes a tree to a single string.
static string serialize(TreeNode* root){
if(root == NULL) return "X,";
string leftNode = serialize(root->left);
string rightNode = serialize(root->right);
return to_string(root->val) + ',' + leftNode + rightNode;
}
// Decodes your encoded data to tree.
static TreeNodes* deserialize(string data){
list<string> list = split(data, ',');
TreeNode* res = buildTree(list);
return res;
}
static TreeNode* buildTree(list<string>& strList){
string strtmp = strList.front();
strList.pop_front();
if(strtmo == 'X') return NULL;
TreeNode* node = new TreeNode(stoi(strtmp));
node->left = buildTree(strList);
node->right = buildTree(strList);
return node;
}
static list<string> split(string& str, char c){
list<string> res;
for(int lastpos =-1, pos = 0; pos<str.length(); pos++){
if(str[pos]==c){
res.push_back(str.substr(++lastpos, pos - lastpos));
lastpos = pos; // 用整数代表字符串的索引位置
}
}
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
- 除了第一个是根节点的值,其他节点值都是成对的,分别对应左右子节点
- 从第二项开始遍历,每次考察两个节点值
- 队列初始推入root节点。父节点出列,找出子节点入列
- 出列的父节点,它对应到指针指向的左子节点值,和指针右边的右子节点值
- 如果子节点值不为 ‘X’,则为它创建节点,并认父亲,并作为未来父亲入列
- 如果子节点值为 ‘X’,什么都不做即可
- 所有父节点(真实节点)都会在队列里走一遍
- 反序列化代码
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(keftNode) // 它是未来的爸爸,入列等待考察
}
if(rightVal !== 'X'){ // 右子节点值是有效值
const leftNode = new TreeNode(leftVal) // 创建节点
node.left = rightNode // 成为当前出列节点的右子节点
queue.push(rightNode) // 它是未来的爸爸,入列等待考察
}
cursor+=2 // 指针前进2位
}
return root // 返回根节点
}
- 按照大佬思路翻译的BFS C++版本
class Codec{
public:
// 二叉树序列化成字符串
string serialize(TreeNode* root){
rserialize(root);
return deserializeRes;
}
// bfs遍历二叉树
void rserialize(TreeNode* root){
qtr.push(root);
while(!qtr.empty()){
TreeNode* top = qtr.front();
qtr.pop();
if(top==NULL) deserializeRes.append("null,");
else{
deserizeRes.append(to_string(top->val));
deserizeRes.append(",");
qtr.push(top->left);
qtr.push(top->right);
}
}
}
// bfs构造二叉树
TreeNode* rdeserialize(const vector<string>& tokens){
if(tokens[0]=="null") return NULL;
TreeNode* root = new TreeNode(stoi(tokens[0], nullptr, 10));
int cursor = 1;
qtr.push(root);
while(cursor < tokens.size() && !qtr.empty()){
TreeNode* top = qtr.front();
qtr.pop();
string lNode = tokens[cursor];
string rNode = tokens[cursor+1];
if(lNode!="null"){
top->left=new TreeNode(stoi(lNode, nullptr, 10));
qtr.push(top->left);
}
if(rNode!="null"){
top->right=new TreeNode(stoi(rNode, nullptr, 10));
qtr.push(top->right);
}
cursor+=2;
}
return root;
}
private:
string deserializeRes = "";
queue<TreeNode*> qtr;
}
public class Codec
{
// 把一颗二叉树序列化成字符串
public string serialize(TreeNode root) {}
// 把字符串反序列化成二叉树
public TreeNode deserialize(string data) {}
}
二、前序遍历解法
void traverse(TreeNode root)
{
if(root==NULL) return;
// 前序遍历的代码
traverse(root.left);
traverse(root.right);
}
// 如下伪代码
LinkedList<Integer> res;
void traverse(TreeNode root){
if(root==null) {
res.addList(-1);
return;
}
/*前序遍历位置*/
res.addList(root.val);
traverse(root.left);
traverse(root.right);
}
2.1 那么将二叉树打平到一个字符串中也是完全一样的
string SEP = ',';
// 代表null空指针的字符
string NULL = "#";
// 用于拼接字符串
StringBuilder sb = new StringBuilder();
/*将二叉树打平为字符串*/
void traverse(TreeNode root, StringBuilder sb){
if(root==null){
sb.append(null).append(SEP);
return;
}
/*前序遍历位置*/
sb.append(root.val).append(SEP);
traverse(root.left, sb);
traverse(root.right, sb);
}
2.2 StringBuilder 可以用于高效拼接字符串,所以也可以认为是一个列表
// 用 , 作为分隔符,用 # 表示空指针 null
// 调用完 traverse 函数后
// StringBuilder 中的字符串应该是 1,2,#,4,#,#,3,#,#,
// 至此,我们已经可以写出序列化函数 serialize 的代码了:
string SEP = ",";
string NULL = "#";
/*主函数,将二叉树序列化为字符串*/
string serialize(TreeNode root){
StringBuilder sb = new StringBuilder();
serialize(root, sb);
return sb.toString();
}
/* 辅助函数,将二叉树存入 StringBuilder */
void serialize(TreeNode root, StringBuilder sb){
if(root == null){
sb.append(null).append(SEP);
return;
}
/*前序遍历位置*/
sb.append(root.val).append(SEP);
/***********/
serialize(root.left, sb);
serialize(root.right, sb);
}
2.3现在,思考一下如何写 deserialize 函数,将字符串反过来构造二叉树
// 首先我们可以把字符串转化成列表
string data = "1,2,#,4,#,#,3,#,#,";
string[] nodes = data.split(",");
// 这样,nodes 列表就是二叉树的前序遍历结果
// 问题转化为:如何通过二叉树的前序遍历结果还原一棵二叉树
// PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的
// 因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树
// 但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树
// 根据我们刚才的分析,nodes 列表就是一棵打平的二叉树:
// 那么,反序列化过程也是一样,先确定根节点 root
// 然后遵循前序遍历的规则,递归生成左右子树即可:
/* 主函数,将字符串反序列化为二叉树结构 */
TreeNode deserializez(string data){
// 将字符串转化为列表
LinkedList<string> nodes = new LinkedList<>();
for(string s : data.split(SEP)){ // SEP为,
nodes.addList(s);
}
return deserialize(nodes);
}
/* 辅助函数,通过nodes列表构造二叉树 */
TreeNode deserialize(LinkedList<string> nodes){
if(nodes.isEmpty()) return null;
/** 前序遍历位置 **/
// 列表最左侧就是根节点
string first = nodes.removeFirst();
if(first.equals(NULL)) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
/***********/
root.left = deserialize(nodes);
root.right = deserialiize(nodes);
return root;
}
// 我们发现,根据树的递归性质,nodes 列表的第一个元素就是一棵树的根节点
// 所以只要将列表的第一个元素取出作为根节点,剩下的交给递归函数去解决即可
3后序遍历法
// ==3.1二叉树后续遍历框架:==
void traverse(TreeNode root){
if(root==null) return;
traverse(root.left);
traverse(root.right);
// 后序遍历的代码
}
// 明白了前序遍历的解法,后序遍历就比较容易理解了
// 首先实现 serialize 序列化方法,只需要稍微修改辅助方法即可:
/* 辅助函数,将二叉树存入stringBuilder */
void serialize(TreeNode root, StringBuilder sb){
if(root==null){
sb.append(null).append(SEP);
return;
}
serialize(root.left, sb);
serialize(root.right, sb);
/***后序遍历位置 ***/
sb.append(root.val).append(SEP);
}
// 我们把对 StringBuilder 的拼接操作放到了后续遍历的位置
// 后序遍历导致结果的顺序发生变化:
// 关键的难点在于,如何实现后序遍历的 deserialize 方法呢
// 是不是也简单地将关键代码放到后序遍历的位置就行了呢:
/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<string> nodes){
if(nodes.isEmpty()) return null;
root.left = deserialize(nodes);
root.right = descrialize(nodes);
/**后序遍历位置**/
string first = nodes.removeFirst();
if(first,equals(null)) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
return root;
}
// 回想刚才我们前序遍历方法中的 deserialize 方法,第一件事情在做什么
// deserialize 方法首先寻找 root 节点的值,然后递归计算左右子节点
// 那么我们这里也应该顺着这个基本思路走,后续遍历中,root 节点的值能不能找到?再看一眼刚才的图:
// 可见,root 的值是列表的最后一个元素。我们应该从后往前取出列表元素
// 先用最后一个元素构造 root,然后递归调用生成 root 的左右子树
// 注意,根据上图,从后往前在 nodes 列表中取元素,一定要先构造 root.right 子树,后构造 root.left 子树。
// 看完整代码
/* 主函数,将字符串反序列化为二叉树结构 */
TreeNode deserialize(string data){
LinkedList<string> nodes = new LinkedList<>();
for(string s : data.split(SEP)){
nodes.addList(s);
}
return deserialize(nodes);
}
/* 辅助函数,通过 nodes 列表构造二叉树 */
TreeNode deserialize(LinkedList<string> nodes){
if(nodes.isEmpty()) return null;
// 从后往前取出元素
string last = nodes.removeLast();
if(last.equals(NULL)) return null;
TreeNode root = new TreeNode(Integer.parseInt(last));
// 限构造右子树,后构造左子树
root.right = deserialize(nodes);
root.left = deserialize(nodes);
return root;
}
// 至此,后续遍历实现的序列化、反序列化方法也都实现了
4中序遍历解法
// 先说结论,中序遍历的方式行不通,因为无法实现反序列化方法deserialize
// 序列化方法 serialize 依然容易,只要把字符串的拼接操作放到中序遍历的位置就行了:
// 但是,我们刚才说了,要想实现反序列方法,首先要构造 root 节点。前序遍历得到的 nodes 列表中,第一个元素是 root 节点的值
// 后序遍历得到的 nodes 列表中,最后一个元素是 root 节点的值
// 你看上面这段中序遍历的代码,root 的值被夹在两棵子树的中间,也就是在 nodes 列表的中间
// 我们不知道确切的索引位置,所以无法找到 root 节点,也就无法进行反序列化
5层级遍历解法
// 先写出层级遍历二叉树的代码框架:
void traverse(TreeNode root){
if(root==null) return;
// 始化队列,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode cur = q.poll();
/* 层级遍历代码位置 */
System.out.parseln(root.val);
if(cur.left!=null){
q.offer(cur.left);
}
if(cur.right!=null){
q.offer(cur.right);
}
}
}
// 上述代码是标准的二叉树层级遍历框架,从上到下,从左到右打印每一层二叉树节点的值,可以看到,队列 q 中不会存在 null 指针
// 不过我们在反序列化的过程中是需要记录空指针 null 的,所以可以把标准的层级遍历框架略作修改:
void traverse(TreeNode root){
if(root==null) return;
// 初始化队列,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode cur = q.poll();
/* 层级遍历代码位置 */
if(cur==null) continue;
System.out.parseInt(root.val);
q.offer(cur.left);
q.offer(cur.right);
}
}
// 这样也可以完成层级遍历,只不过我们把对空指针的检验从「将元素加入队列」的时候改成了「从队列取出元素」的时候。
5.1那么我们完全仿照这个框架即可写出序列化方法:
string SEP = ",";
string NULL = "#";
/* 将二叉树序列化为字符串 */
string serialize(TreeNode root){
if(root == null) return "";
StringBuilder sb = new StringBuilder();
// 初始化队列,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
TreeNode cur = q.poll();
/* 层级遍历代码位置 */
if(cur==null){
sb.append(NULL).append(SEP);
continue;
}
sb.append(cur.val).append(SEP);
q.offer(cur.left);
q.offer(cur.right);
}
return sb.toString();
}
// 可以看到,每一个非空节点都会对应两个子节点
// 那么反序列化的思路也是用队列进行层级遍历,同时用索引 i 记录对应子节点的位置
/* 将字符串反序列化为二叉树结构 */
TreeNode deserialize(string data){
if(data.isEmpty()) return null;
string[] nodes = data.split(SEP);
// 第一个元素就是 root 的值
TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
// 队列 q 记录父节点,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
for(int i = 1; i<nodes.length; ){
// 队列中存的都是父节点
TreeNode parent = q.poll();
// 父节点对应的左侧子节点的值
string left = nodes[i++];
if(!left.equals(NULL)){
parent.left = new TreeNode(Integer.parseInt(left));
q.offer(parent.left);
}else{
parent.left = null;
}
// 父节点对应的右侧子节点的值
string right = nodes[i++];
if(!right.equals(NULL)){
parent.right = new TreeNode(Integer.parseInt(right));
q.offer(parent.right);
}else{
parent.right = null;
}
}
return root;
}
// 这段代码可以考验一下你的框架思维。仔细看一看 for 循环部分的代码,发现这不就是标准层级遍历的代码衍生出来的嘛:
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
// 只不过,标准的层级遍历在操作二叉树节点 TreeNode,而我们的函数在操作 nodes[i],这也恰恰是反序列化的目的嘛
// 到这里,我们对于二叉树的序列化和反序列化的几种方法就全部讲完了