二叉树的序列化与反序列化的一些思考

来源:互联网 发布:滑坡灾害数据 编辑:程序博客网 时间:2024/06/07 23:33

      这已经是一个老问题了,将二叉树序列化(将对象的状态信息转换为可以存储或传输的形式的过程)到文件中,以及将文件中的数据恢复到内存中的树形结构。

      前一段时间实验室的几个小伙伴在做这个问题,他们用的方法是我们下面称之为方法二的方式做的,大概思想就是用0标记空指针域,然后用先序遍历的方式序列和反序列。而当时我看到这个问题的第一反应就是利用二叉树的前序遍历和中序遍历唯一确定一棵二叉树,这也是数据结构课上学到的,然而自己并没有亲自实现过,只记得按书上的例子是可以完成的。我们暂时称这种方式为方法一。

      方法一:例如给出一个树:

                              a

                         /         \

                       b            c

                    /      \      /      \

                 d         e    f        g

      前序遍历是:  abdecfg

      中序遍历是:  dbeafcg

      我们知道前序遍历是先根再左子树最后右子树的遍历方式递归进行的,所以前序遍历第一个节点a即为根节点。而中序遍历是按照先左子树再根节点最后右子树的方式递归进行的,所以我们以a为分割点,则(dbe)为根节点a的左子树;(fcg)为a的右子树。然后我们继续分析下去,前序遍历的第二个节点是b,既然从中序遍历中我们得到的a的左子树不为空,那么b肯定是a的左子树的根节点,然后再用b对a的左子树中序(dbe)进行分割。这样递归下去,我们就可以得到这棵唯一的二叉树了。那么我们就愉快地得出了第一种方法。

      故事本不应这么轻松愉快地发展,但是由于我们上面的例子(也是大多数教材中的例子)太过于简单粗暴了,一些细节问题被忽略了。我们来看看下面两棵二叉树:

                          c                                                        c

                      /       \                                                /         \

                   d           a                                           d            c

                /     \       /                                                  \             \ 

             d         c  b                                                    d            a

                                                                                             /

                                                                                           b

    前序:cddcab                                             前序:cddcab

    中序:ddccba                                             中序:ddccba

    你没有看错,上面两棵树的前序和中序遍历结果是一样的……然而两棵树却是完全不同,如果我们用方法一的话,可能你要输出的是树1,而恢复进来就成了树2了,这肯定不是我们期望的。出现这种问题的原因就在于树的节点内容重复了。也许有人要说没有重复,那两个d是不一样的,但是在以前序中序遍历输出的结果看,我们是分辨不出来的。既然二叉树没有规定不能有重复元素,那我们这种方法一注定是要失败的。

     补救方法:当然也不是完全没有补救方法,只要我们将那些重复元素区分开来就行了,例如输出时候给重复元素加上序号,读入后再分别处理也是可以的。只不过要牺牲其简便性了。

    代价:如果不考虑补救方式加的标记,这种方式序列化的浪费的空间为 n (n为节点个数)。

方法二:

    这种方式就是实验室小伙伴用的也是网上广为流传的方法了,就是用一个特殊字符,如#,来标记二叉树的NULL指针。我们在序列化的时候,即采用先序遍历的方式,遇到孩子节点指针为NULL则输出#,否则输出节点数据。反序列化时,在每遇到一个数据时,我们就根据根、左、右的顺序来构建树,若为普通数据,则构建节点,若为标记数据,则置空。当前子节点构建完成后,回溯父节点,直到回溯到根节点,且已经构建完成。

    本来看到这种方法的时候,本能排斥是因为觉得它的冗余数据(即标记数据)太多了。然而仔细一想,却没有那么严重。我们考虑两个极端,满二叉树和单枝下去的二叉树。这两个补充的冗余数据都为n+1。满二叉树:最后的叶子节点全部补两个,很容易知道最后一排的节点数等于上面所有层的总和+1。单枝的就更直接了……

    如果你不能信服的话,我们可以按二叉树的度来分析。设度为0、1、2的节点分别为n0,n1,n2。我们知道n0=n2+1. 我们补充特殊标记的目标是将度不满2的节点补到2.那么我们总共需补充2×n0 + n1 = n0 + n2 + n1 + 1 个冗余量。而总节点数为 n0 + n1 + n2 ,所以我们仅需补充n+1个冗余量。

    问题: 作为特殊标记的量不能出现在数据中,否则这种方法就崩了…… 暂没想到解决方式。

    推广:如果是多叉树,比如M叉树,方法二的冗余量可就很多了,可以证明冗余信息量是(M-1)×n + 1 ,而方法一始终是n,那是不是代表我们要用方法一了。其实……你想多了,多叉树的中序遍历是什么??

参考文章:http://leetcode.com/2010/09/serializationdeserialization-of-binary.html

0 0
原创粉丝点击