高频算法面试题(四十六)- 二叉树的序列化与反序列化

216 阅读4分钟

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

题目来源LeetCode-297. 二叉树的序列化与反序列化

题目描述

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构

输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题

示例

示例 1

1.png

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

示例 2

输入:root = []
输出:[]

示例 3

输入:root = [1]
输出:[1]

示例 4

输入:root = [1,2]
输出:[1,2]

提示:

  • 树中结点数在范围 [0, 104] 内
  • 1000 <= Node.val <= 1000

解题

解法一:深度优先搜索

思路

序列化是将一个数据结构或者对象转换为连续的比特位的操作

这道题本质上其实就是遍历二叉树,不一样的地方是,你需要能将遍历出来的结果还原成一个二叉树。本题分两步来走,第一步是序列化,第二步是反序列化

序列化

序列化的过程其实就是二叉树的遍历,二叉树的遍历就那几种

  • 前序遍历
  • 中序遍历
  • 后续遍历
  • 层序遍历

关于二叉树的各种遍历的递归实现及非递归实现,可以看这里

我这里选择前序遍历来实现二叉树的遍历。实现的过程是利用深度优先的思想,也就是从根节点开始遍历,到叶子结点,然后再回到根节点,遍历根节点的右子树

在遍历的过程中,将结点的值通过逗号分隔,组成字符串,遇到空结点,用字符串"nil"标识。下边用图展示一下过程(以例1为例)

2.png

反序列化

反序列化的过程是将序列化的结果,还原成原来的二叉树

根据前序遍历的结果知道,第一个是跟结点,我们可以根据序列化的结果,从左往右进行扫描

  • 如果当前扫到的元素不是nil,则以该元素为值创建节点,然后再依次解析它的左右子树
  • 如果当前扫到的元素是ni,当前节点为空

代码

// 二叉树的序列化与反序列化
//深度优先搜索
func Serialize(root *TreeNode) string {
	serializeStr := &strings.Builder{}
	dfsSerialize(root, serializeStr)

	return serializeStr.String()
}

func dfsSerialize(node *TreeNode, serializeStr *strings.Builder)  {
	if node == nil {
		serializeStr.WriteString("nil,")
		return
	}
	serializeStr.WriteString(strconv.Itoa(node.Val))
	serializeStr.WriteByte(',')
	dfsSerialize(node.Left, serializeStr)
	dfsSerialize(node.Right, serializeStr)
}

func Deserialize(data string) *TreeNode {
	deserializeArr := strings.Split(data, ",")
	var buildBinaryTree func() *TreeNode
	buildBinaryTree = func() *TreeNode {
		if len(deserializeArr) == 0 {
			return nil
		}
		if deserializeArr[0] == "nil" {
			deserializeArr = deserializeArr[1:]
			return nil
		}
		fmt.Printf("get:%v\n", deserializeArr)

		value, _ := strconv.Atoi(deserializeArr[0])
		deserializeArr = deserializeArr[1:]

		return &TreeNode{value, buildBinaryTree(), buildBinaryTree()}
	}

	return buildBinaryTree()
}

func buildBinaryTree(deserializeArr []string) *TreeNode {
	if deserializeArr[0] == "nil" {
		deserializeArr = deserializeArr[1:]
		return nil
	}
	fmt.Printf("get:%v\n", deserializeArr)

	value, _ := strconv.Atoi(deserializeArr[0])
	deserializeArr = deserializeArr[1:]

	return &TreeNode{value, buildBinaryTree(deserializeArr), buildBinaryTree(deserializeArr)}
}

解法二:括号表示编码 + 递归下降解码

思路

💡 说明:参考官方的第二种解法,官方解释的比较抽象,这里对一些地方做了一些解释

就像编译器中,语法解析阶段进行语法扫描一样。编译器之所以可以自动的去分析语言,是因为将一些文法提供给了它,根据这些文法,程序在运行的时候才知道要干什么

在编译原理中,其中有一个使用非常广泛的描述语法的方式:上下文无关文法BNF(巴科斯范式)。它不仅可以描述一个语言的语法,还可以指导程序的翻译。关于文法详细的内容,可以看《编译原理》的第四章

这道题就可以利用这种思想来解,我们可以按照一种规则来表示一棵二叉树,然后按照这种方法来序列化这颗二叉树。假设按照如下规则:

  • 如果当前结点为空,则用 X表示

  • 如果当前结点不为空,则把它表示为(<LEFT_SUB_TREE>)CUR_NUM(RIGHT_SUB_TREE)

    • <LEFT_SUB_TREE> 是左子树序列化之后的结果
    • <RIGHT_SUB_TREE> 是右子树序列化之后的结果
    • CUR_NUM 是当前节点的值

通俗点来说,就是如果当前结点不为空,则将它左子树序列化的结果用括号括起来,然后连接上当前结点的值,再将右子树序列化的结果用括号括起来,并和前边的部分连接上(其中左右子树的序列化,也是同样的规则)

3.png

所以,这个文法是LL(1)型文法,我们知道文法其实就是一种解析规则,LL(1)文法就可以理解成定义一种递归的方法,并且保证了这个方法的正确性。因此,反序列化的过程,就可以设计一个如下的递归函数

💡 说明:第一个L表示从左往右扫描;第二个L表示产生最左推到;1表示在每一步中只需要向前看一个输入符号来决定解析的行为
  • 如果当前位置为 X 说明解析到了一棵空树,直接返回
  • 否则当前位置一定是 (,对括号内部按照 (T) num (T) 的模式解析

代码

//括号表示编码 + 递归下降解码
func Serialize1(root *TreeNode) string {
	if root == nil {
		return "X"
	}
	left := "(" + Serialize1(root.Left) + ")"
	right := "(" + Serialize1(root.Right) + ")"
	return left + strconv.Itoa(root.Val) + right
}

func Deserialize1(data string) *TreeNode {
	var parse func() *TreeNode
	parse = func() *TreeNode {
		if data[0] == 'X' {
			data = data[1:]
			return nil
		}
		node := &TreeNode{}
		data = data[1:] // 跳过左括号
		node.Left = parse()
		data = data[1:] // 跳过右括号
		i := 0
		//二叉树的结点可能是多位数
		for data[i] == '-' || '0' <= data[i] && data[i] <= '9' {
			i++
		}
		node.Val, _ = strconv.Atoi(data[:i]) //左子树解析完之后,获取节点值
		data = data[i:]
		//解析右子树
		data = data[1:] // 跳过左括号
		node.Right = parse()
		data = data[1:] // 跳过右括号
		return node
	}
	return parse()
}