序列化和反序列化二叉树

序列化和反序列化二叉树

二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。

而我们知道,仅由简单的前中后序遍历的结果是不能恢复出原二叉树结构的.因为丢失了空节点的信息.

因此我们要将空节点用一种特殊的符号标记.

(以下示例对问题做了一些简化:假设二叉树的节点值用一个字符表示,空节点用"#"表示)

以上图所示二叉树为例:

前序序列化: 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;
        }
    }

新手,望多指教!

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值