二叉树的序列化和反序列化

二叉树的序列化和反序列化

一. 前言

JSON 的运用非常广泛,比如我们经常将变成语言中的结构体序列化成 JSON 字符串,存入缓存或者通过网络发送给远端服务,消费者接受 JSON 字符串然后进行反序列化,就可以得到原始数据了。这就是「序列化」和「反序列化」的目的,以某种固定格式组织字符串,使得数据可以独立于编程语言。

那么假设现在有一棵用 Java 实现的二叉树,我想把它序列化字符串,然后用 Java读取这棵并还原这棵二叉树的结构,怎么办?这就需要对二叉树进行「序列化」和「反序列化」了。

二.题目描述

leetcode 297题,给你输入一棵二叉树的根节点root,要求实现如下一个类:

public class Codec {

    // 把一棵二叉树序列化成字符串
    public String serialize(TreeNode root) {}

    // 把字符串反序列化成二叉树
    public TreeNode deserialize(String data) {}
}

serialize 方法也许会把它序列化成字符串 2,1,#,6,3,#,#,其中 # 表示 null 指针,那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。

想象一下,二叉树结该是一个二维平面内的结构,而序列化出来的字符串是一个线性的一维结构。所谓的序列化不过就是把结构化的数据「打平」,其实就是在考察二叉树的遍历方式。

二叉树的遍历方式有哪些?递归遍历方式有前序遍历,中序遍历,后序遍历;迭代方式一般是层级遍历。本文就把这些方式都尝试一遍,来实现 serialize 方法和 deserialize 方法。

三.前序遍历解法

序列化过程:

String SEP =",";
String NULL = "#";
public String serialize(TreeNode root){
	StringBuilder sb =new StringBuilder();
	serialize(root,sb);
	return sb.toString();
}

public 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);
}

在这里插入图片描述

反序列化过程:

//主函数:将字符串反序列化为二叉树结构
TreeNode deserialize(String data){
	//将字符串转换为列表
	LinkedList<String> nodes = new LinkedList<>();
	for(String s:data.split(SEP)){
		nodes.addLast(s);
	}
	return deserialize(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 = deserialize(nodes);
	return root;
}

nodes列表的第一个元素就是一颗子树的根节点,只要将列表的第一个元素取出作为根节点,剩下的交给递归函数即可。

四.后序遍历解法

序列化过程和前序遍历没有什么不同,只是append的位置发生了变化:

String SEP =",";
String NULL = "#";
public String serialize(TreeNode root){
	StringBuilder sb =new StringBuilder();
	serialize(root,sb);
	return sb.toString();
}

public 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);
}

在这里插入图片描述

可见,root 的值是列表的最后一个元素。我们应该从后往前取出列表元素,先用最后一个元素构造 root,然后递归调用生成 root 的左右子树。注意,根据上图,从后往前在 nodes 列表中取元素,一定要先构造 root.right 子树,后构造 root.left 子树。

反序列化过程:

public TreeNode deserialize(String data){
	LinkedList<String> nodes = new LinkedList<>();
	for(String s:data.split(SEP)){
		nodes.addLast(s);
	}
	return deserialize(nodes);
}

public TreeNode deserialize(LinkedList<String>nodes){
	if(nodes.isEmpty()) return null;
	String first = nodes.removeLast();
	if(first.equals(NULL)) return null;
	TreeNode root = new TreeNode(Integer.parseInt(first));
	root.right = deserialize(nodes);
	root.left = deserialize(nodes);
	return root;

}

五.层级遍历解法

中序遍历无法进行反序列化,因为无法确定root的位置。

首先,写出层级遍历的解法:

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.println(root.val);
		if(cur.left!=null)
			q.offer(cur.left);
		if(cur.right!=null)
			q.offer(cur.right);
	}
}

如果需要记录空指针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.println(root.val);
		q.offer(cur.left);
		q.offer(cur.right);
	}
}

把对空指针的检验从「将元素加入队列」的时候改成了「从队列取出元素」的时候。

根据框架,对应的序列化方法:

String SEP=",";
String NULL ="#";
public String serialize(TreeNode root){
	if(root ==null) return "";
	StringBUilder sb = new StringBuilder();
	//初始化队列,将root加入队列
	Queue<TreeNode> q = new LinkedList<>();
	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); //注意这里定义了一个nodes数组
	TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
	//队列q记录父节点,将root加入队列中
	Queue<String> q = new LinkedList<>();
	for (int i=0;i<nodes.length;){
		TreeNode parent =q.poll();
		String left =nodes[i++];
		if(!left.equals(NULL)){
			TreeNode root = new TreeNode(Integer.parseInt(nodes(left));
			q.offer(parent.left);
		}
		else
			parent.right =null;
		if(!right.equals(NULL)){
			TreeNode root = new TreeNode(Integer.parseInt(nodes(right));
			q.offer(parent.right);
		}
		else
			parent.right =null;
	}
	return root;
}

这实际上就是层级遍历的代码衍生出来的:

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]。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值