序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。
而我们知道,仅由简单的前中后序遍历的结果是不能恢复出原二叉树结构的.因为丢失了空节点的信息.
因此我们要将空节点用一种特殊的符号标记.
(以下示例对问题做了一些简化:假设二叉树的节点值用一个字符表示,空节点用"#"表示)
以上图所示二叉树为例:
前序序列化: 124##5##36##7##
中序序列化: #4#2#5#1#6#3#7#(无法恢复出原二叉树,后文有解释)
后序序列化: ##4##52##6##731
-
序列化二叉树
1.前序序列化
public static String preorderTraversal(TreeNode root){//前序遍历
if(root==null){
return "#";
}
return root.val+preorderTraversal(root.left)+preorderTraversal(root.right);//中左右
}
前序序列化过程:
1.空节点返回”#”;
2.将当前节点加入字符串序列中; //中
3.将当前节点左子节点的序列化字符串加入返回的字符串序列中; //左
4.将当前节点右子节点的序列化字符串加入返回的字符串序列中; //右
2.中序序列化
public static String inorderTraversal(TreeNode root){//中序遍历
if(root==null){
return "#";
}
return inorderTraversal(root.left)+root.val+inorderTraversal(root.right);//左中右
}
3.后序序列化
public static String postorderTraversal(TreeNode root){//后序遍历
if(root==null){
return "#";
}
return postorderTraversal(root.left)+postorderTraversal(root.right)+root.val;//左右中
}
可以看出,前中后序序列化的区别在于将当前节点值加入字符串的时机.
-
反序列化二叉树
1.前序反序列化
分析:前序序列化时,将二叉树的根节点放在序列的最前方,因此以上图二叉树为例,根节点就是”1”.
序列化根节点后,紧接着序列化左子树,因此”2”为左子树的根节点.
序列化左子树后,序列化右子树,”3”为右子树根节点(事后看出来的).
”2”放入递归反序列化程序,返回的就是左子树的根节点;返回左子树根节点后,数组指针来到”3”位置.
“3”放入递归反序列化程序,返回的就是右子树的根节点.
将根节点和其左右子树组装起来,返回.
static int root_pre =0;
public static TreeNode preorderDeSerialize(char[] str){//前序反序列化
//str[root_pre]代表当前子树的根节点,由于是前序遍历,根节点就在字符串的首位
if(str[root_pre]=='#'){
root_pre++;
return null;
}
//复原当前根节点
TreeNode newnode=new TreeNode(Integer.parseInt(str[root_pre]+""));
root_pre++;//千万别忘了用过该数之后调整指针
TreeNode leftnode= preorderDeSerialize(str);//由于前序遍历记录根节点后记录的是左子节点,反序列化时依然要按照该顺序
TreeNode rightnode= preorderDeSerialize(str);//当左子节点的递归返回后,root_pre自然就指向了右子数的根节点
newnode.left=leftnode;//完成二叉树的组装
newnode.right=rightnode;
return newnode;//返回
}
2.后序反序列化
分析:后序序列化时,将二叉树的根节点放在序列的末尾,因此以上图二叉树为例,根节点是”1”.
在序列化根节点时,紧前面就是根节点右子数的序列(左右中嘛),而右子树序列的末尾又是右子树的根节点,因此”3”为右子树的根节点.
递归反序列化以”3”为根节点的右子树后,返回右子树根节点,指针自然来到了”2”.
因此”2”为左子树的根节点, 递归反序列化以”2”为根节点的左子树,指针耗尽.
将根节点和其左右子树组装起来,返回.
static int root_post;
public static TreeNode postorderDeSerialize(char[] str){//后序反序列化
root_post =str.length-1;//后序序列化时,二叉树的根节点被放在字符串序列的最后,因此后序反序列化应该从字符串的末位开始
return postorderDeSerializeProcess(str);
}
//后序序列化时,放置的顺序是"左右中",因此后序反序列化应该从后往前一次解析"中右左"
public static TreeNode postorderDeSerializeProcess(char[] str){
//str[root_post]代表当前子树的根节点,由于是后序遍历,根节点就在字符串的末位
if(str[root_post]=='#'){
root_post--;
return null;
}
//复原当前根节点
TreeNode newnode=new TreeNode(Integer.parseInt(str[root_post]+""));//中
root_post--;//千万别忘了用过该数之后调整指针
TreeNode rightnode= postorderDeSerializeProcess(str);//右,由于是后序序列化,当前字符的前一个字符是本节点右子树的根节点
TreeNode leftnode= postorderDeSerializeProcess(str);//左,当右子树递归返回后,root_post指向的就是当前节点左子树的根节点
newnode.left=leftnode;//完成二叉树的组装
newnode.right=rightnode;
return newnode;//返回
}
3.中序反序列化
最烧脑的部分来了:上述的中序序列化方式可以反序列化吗?答案是不能!
由于上述的中序序列化方式生成的字符串有一个一致的模式,那就是:# [ ] # [ ] # [ ] # [ ] # [ ] #
分析该特点的形成原因:
从中序序列化的代码中就可以看出,每次调用递归函数返回的都是:
inorderTraversal(root.left)+root.val+inorderTraversal(root.right),或者是叶子节点返回的”#”
最早从空节点返回”#”;
到上层叶节点返回”# [叶] #”;
再到非叶节点返回” # [非叶] # [叶] # ”,” # [叶] # [非叶] # ”或” # [叶] # [非叶] # [叶] #”
分别对应非叶节点的三种情形:只有右子节点,只有左子节点和左右子节点都有.
因此出现了此种特殊的模式.
于是就导致了某种信息的损失:
因为既然此种中序序列化有这种特点,以至于由中序遍历”4251637”直接在首尾和字符之间加入”#”即可获得,如果这样获得的序列可以反序列化出原二叉树,不就相当于普通的中序遍历就可以包含二叉树的结构信息了吗?
显然, 普通的中序遍历不包含二叉树的结构信息!
究竟丢失了什么信息?
仔细思考前后序的反序列化过程:
前序反序列化从序列的首位开始,因为首位是二叉树的根节点;
后序反序列化从序列的末位开始,因为末位是二叉树的根节点;
那中序序列化的根节点在哪?(当然不是在字符串的中间位置,只是上图的示例比较特殊罢了).
丢失了二叉树根的所在位置
前序序列化把根放在最前面,
左子树的根紧跟着根节点放置,
右子树的根,在左子树递归反序列化将字符串消耗一部分之后,也是出现在最前面;
后序序列化把根放在最后面,
右子树的根在根节点前放置,
左子树的根,在右子树递归反序列化将字符串消耗一部分之后,也是出现在最后面;
而中序序列化时,由于左右子树上的节点不一定相等,字符串的中间位置不一定就是根节点坐在位置.
于是根位置信息丢失!
额外标记一下根的位置呢?
注意是每次递归反序列化都需要根的位置信息,因此能解决该问题就可以恢复出原二叉树.
既然
前序遍历”1245367”中包含根节点位置信息,但不包含空节点信息;
中序遍历”4251637”可轻易地通过字符填充变为”#4#2#5#1#6#3#7#”从而恢复空节点信息;
结合两种遍历方式是不是可以恢复出原二叉树呢?
可以!
如上例中,由前序遍历结果可知,根节点为1,且左子树根节点为2;
由于根节点为1,在中序遍历的结果中可以确定二叉树的结构:1左边有三个数,即1节点左子树大小为三;同理,右子树大小为三.
再由左子树大小为三可判断,245为左子树的前序遍历结果,因此3即为右子树的根节点.
将中序遍历的左子树425结合根节点为2的信息传入递归,返回左子节点;
将中序遍历的右子树637结合根节点为3的信息传入递归,返回右子节点;
重组二叉树返回即可;
public static int find(char[] chars,char dst){//返回特定字符的位置
return new String(chars).indexOf(dst);
}
public static TreeNode reconstructByPreAndIn(char[] pre_arr, char[] in_arr) {
return reconstructByPreAndIn(0,pre_arr,in_arr);
}
public static TreeNode reconstructByPreAndIn(int cur_root, char[] pre_arr, char[] in_arr) {//pre_arr[cur_root]为本次递归的根节点
if(in_arr.length==0){
return null;
}
int index=find(in_arr,pre_arr[cur_root]);//在中序数组中找到根所在位置index
char[] left_in_arr=new char[index];//中序左子串
char[] right_in_arr=new char[in_arr.length-index-1];//中序右子串
System.arraycopy(in_arr,0,left_in_arr,0,index);
System.arraycopy(in_arr,index+1,right_in_arr,0,in_arr.length-index-1);
TreeNode left= reconstructByPreAndIn(cur_root+1,pre_arr,left_in_arr);//左子树的根节点为pre_arr[cur_root+1]
TreeNode newnode=new TreeNode(Integer.parseInt(pre_arr[cur_root]+""));//恢复当前根节点
TreeNode right= reconstructByPreAndIn(cur_root+index+1,pre_arr,right_in_arr);//右子树的根节点为pre_arr[cur_root+index+1]
newnode.left=left;//重组二叉树
newnode.right=right;
return newnode;//返回根节点
}
附:二叉树节点的定义:
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
新手,望多指教!