项目地址:https://github.com/SpecialYy/Sword-Means-Offer
问题
请实现两个函数,分别用来序列化和反序列化二叉树。
解析
首先要了解什么是序列化。
- 序列化指的是将对象转换成字节码形式或其他文本格式以方便我们进行持久化或网络传输。
- 反序列化就是根据转换后的格式恢复成原始对象模型。
理解了以上的定义,再来看题目的意思,目的就是希望能够对一个二叉树生成某种文本格式从而能够描述它,必要时可以根据转化后的文本格式恢复为原始的二叉树结构。
对于二叉树的序列化,一般都是采用二叉树的遍历序列来描述的,用节点的值表示该节点,用’!‘来分割不同的值。对于空节点我们用’#'表示。虽然单纯的某种遍历方式无法确定一棵树,但是我们如果保留了节点的空孩子信息,那么就可以唯一确定一棵树。因为相同的遍历序列且节点的空孩子信息一致只能是同一颗树。那么我们采用哪种遍历方式呢,考虑到反序列(恢复树)的难易程度,先序遍历要简单点,也就是先访问根节点,再访问左右孩子。这种遍历方式非常符合正常人建树的逻辑。
上图的根据先序遍历法则的序列化结果为:1!2!4!###3#7##.
Note: !是用于分割不同值,因为节点的值有可能不是1位,比如12,这样的话我们就无法确定是2个节点还是一个节点。#用于表示空节点,表示对应节点的左或右孩子为空。
思路一
如果你已经明白了预备知识的核心,那么这道题其实就可以分解为2部分:
- 根据先序遍历法则序列化二叉树
- 根据序列化的结果重构原始二叉树
第一部分,可以通过递归和非递归两种方式构建。我们思路一仅仅关注递归形式,因为先序遍历的访问规则为根-左-右,所以我们只需先访问当前节点,然后不断递归遍历左孩子,然后回溯遍历右孩子即可。
重点是如何恢复二叉树,通过第一部分的序列结果,我们可以知道当前序列的第一个值必定为根节点。我们可以根据!就可以确定根节点的值,之后继续考察剩余的序列。剩余的序列必然是左子树和右子树遍历序列,很巧妙的是左子树和右子树的恢复问题跟原问题一样,而且问题规模是不断缩小的,所以我们可以通过递归优雅的实现。只需不断对左子树和右子树确定其对应根节点,然后返回给父节点连接起来即可。
举个栗子:1!2!4!###3#7##
-
首先确定根节点的值为1,生成节点1
-
当前序列变为2!4!###3#7##,此序列是左子树和右子树遍历结果,所以我们要先确定左子树根节点,再确定右子树的根节点。而左子树确定根节点的过程与步骤1一样,所以问题继续缩小,产生节点2
-
当前序列变为4!###3#7##,从而确定2节点的左孩子为4
-
当前序列变为###3#7##,##代表4为叶子节点,左右孩子都为空,这时回溯到上一层,即2节点。
-
当前序列变为#3#7##,这一层之前已经考察过左子树了,这是该考虑的右子树,#代表2的右子树为空,继续向上回溯到1
-
当前序列变为3#7##,这一层之前已经考察过左子树了,这是该考虑的右子树,其右子树的根节点为3
-
当前序列变为#7##,#代表3的左子树为空,则向上回溯到3节点
-
当前序列变为7##,这一层之前已经考察过左子树了,这是该考虑的右子树,其右子树的根节点为7
-
当前序列为##,说明7为叶子节点
note:当某一个的节点的左右孩子都重构后,要记得返回给上一层,这样父节点就可以与左右孩子建立连接。
//方法一:递归形式的先序遍历 StringBuilder sb = new StringBuilder(); String Serialize(TreeNode root) { if (root == null) { return null; } dfs(root); return sb.toString(); } /** * 递归形式的先序遍历 * @param node */ void dfs(TreeNode node) { if (node == null) { sb.append("#"); } else { sb.append(node.val); sb.append("!"); dfs(node.left); dfs(node.right); } } int index = 0; /** * 反序列化 * @param str * @return */ TreeNode Deserialize(String str) { if (str == null || index >= str.length()) { return null; } //当前节点为空 if (str.charAt(index) == '#') { index++; return null; } int val = 0; //截取当前到!之前构造节点值 while (str.charAt(index) != '!') { val = val * 10 + str.charAt(index++) - '0'; } TreeNode node = new TreeNode(val); index++; //生成左子树 node.left = Deserialize(str); //生成右子树 node.right = Deserialize(str); return node; }
方法二
方法二其实就是对第一部分的改进,利用非递归的先序遍历来搞。先序遍历是按“根-左-右”的顺序访问,所以我们此处借用栈来辅助我们先序遍历,具体做法如下
- 首先根节点入栈
- 判断栈是否为空,否,弹出栈顶节点,令节点的右孩子先入栈,后令节点的左孩子入栈
- 继续步骤2,直到栈为空
我们通过这种方式可以始终让左孩子优先弹出于右孩子。这里你肯定会问那干嘛不用队列先入先出,不就能更好让左孩子优先弹出于右孩子了?其实你模拟一下就会发现,利用队列会导致根节点的右子树先与根节点的左孩子的孩子节点先弹出,根据“根-左-右”来看这是不符合先序遍历规则的。所以我们要用栈,右子树先入栈,之后左子树入栈,即可保证左子树访问完了,再访问右子树。
//方法二-------非递归形式的先序遍历 String Serialize1(TreeNode root) { if (root == null) { return null; } Stack<TreeNode> stack = new Stack<>(); stack.push(root); StringBuilder sb = new StringBuilder(); while(!stack.isEmpty()) { TreeNode node = stack.pop(); if (node == null) { sb.append("#"); } else { sb.append(node.val); sb.append("!"); stack.push(node.right); stack.push(node.left); } } return sb.toString(); }
总结
通过序列化即可唯一确定一颗二叉树,那你知道在不通过序列化的方式下,通过几种遍历方式可以确定一棵树呢?