java 二叉树的遍历 为何只给出前序以及后序遍历,不能生成惟一的二叉树

最近在学习java的数据结构与算法知识,看到数据结构 树的遍历的方式。在理解过程当中。查看到一篇文章,视野很是有深度,在信息论的角度看待这个问题。在此贴出该文章的连接以及内容。php

【文章出处】http://www.binarythink.net/2012/12/binary-tree-info-theory/ java

咱们在学习二叉树的遍历时,都会不可避免的学到二叉树的三种遍历方式,分别是遵循(根-左-右)的前序遍历、遵循(左-根-右)的中序遍历以及遵循(左-右-根)的后序遍历。而且每个二叉树均可以用这三种遍历方式而且分别转换为字符串序列,以便在计算机上面保存。可是咱们在进行逆向操做的时候却会遇到困难:咱们并不能从某一种遍历方式推断出惟一的二叉树,也就是说,这是个单向编码的过程。而当咱们有一个二叉树的两种遍历方式的表述时,彷佛也不能作到尽善尽美:前序遍历和中序遍历的组合或者中序遍历和后序遍历的组合能够逆向生成惟一的二叉树,可是恰恰前序遍历和后续遍历却不能够。这其中的缘由是什么呢?算法

1. 哈希函数与逆波兰表达式

A. 哈希映射

在Serverfault举办的一次解谜式游戏中,其中有一关的谜底是将下一关的序号转换成MD5码,以后替换掉原始URL的k值。此处咱们能够经过谷歌找到一些加密文本为MD5的网站来进行加密。好比咱们在网站中输入5,那么咱们就会获得e4da3b7fbbce2345d7772b0674a318d5这个值。因为MD5复杂的算法使得它曾经一度被认为是难以破解的。因此它曾被不少网站用于加密关键数据好比用户密码。也就是说网站的数据库中只保存了加密过的密码,这样即便黑客经过某种手段获得了整个数据库,也由于没法还原那串32位的“乱码”而对密码无从知晓。然而,在1996年,德国的密码学家Hans Dobbertin却发现了MD5加密算法的漏洞使得MD5今后做为保存密码的功能的算法被逐渐废弃。Hans Dobbertin发现的漏洞就在于,存在几个不一样的原文(即未加密的文字),其经过MD5加密后获得的字符序列是相同的。这样的现象在密码学中叫作collision,也就是“碰撞”。正是因为这样的collision的发现,使得人们对MD5忧心忡忡,毕竟,如今甚至不须要知道你的原始密码,也许换几个其余字符,结果也能和你的密码同样进行登陆。数据库

其实,这样的collision的存在其实从一开始就是必然的。由于这个算法会将任意长度的字符串生成为一个32位的序列,也就是说,这个生成的字符串最多只有(26+10)^(32)种可能的状况。而将无穷可数的字符串都映射到这36^10个字符串中,依据鸽巢原理,必然就会存在一些字符串的映射值相同。只不过因为这个算法的复杂,咱们不能从MD5逆推出原始数据的可能,而确实在应用中也没有找到不安全的例子,因此就这样“侥幸”地被用到了安全领域。小程序

而相似于MD5加密所采用的这种将一个大的集合经过某种算法映射到一个小的集合当中的过程,就叫作一个“哈希映射”。熟悉数据结构的同窗必定不会陌生,甚至只是接触过一些Java的同窗也必定对hash这个词有必定的了解。在Java中,若是直接打印输出一个对象,在控制台中就会出现一个这个对象的id,而这个id就是由对象的hashCode方法生成的。Java也运用这个hash的方法来判断两个对象是否相等(有点像上文提到过的判断用户密码是否输入正确)。segmentfault

从信息论的角度来看,一个任意长于32位的字符串经过哈希函数处理后,实质上是完成了一次信息压缩。而当处理以后的信息再解码时不能生成惟一的信息时,咱们就发现这种压缩是有损的。由于当一个肯定信息经过某种处理使它的不肯定性增大时,其包含的信息量也就会减小。安全

B. 树的遍历与逆波兰表达式

到如今咱们就会忽然发现,其实对于一棵树的遍历也能够看作一次哈希映射。只不过此次咱们是将一个具体的有特定结构的树映射为长度为树的节点个数的字符串,而咱们的哈希函数就是同窗们所熟悉的遍历顺序。好比咱们能够构造这样一棵树A(B(D(G()())(H()()))(E()()))(C()(F(I()())())) (这种繁琐的二叉树表示法可参考这里)数据结构

A(B(D(G()())(H()()))(E()()))(C()(F(I()())()))

其经过前序遍历这一哈希函数的处理,咱们就能够获得这个字符串:ABDGHECFI。然而,当咱们将这个前序遍历的字符串进行解码时,咱们却发现没法结果并非惟一的。好比,对于二叉树A(B(D(G()())(H()()))(E()()))(C()(F()(I()())))函数

ABDGHECFI

当咱们经过前序遍历进行哈希映射时,咱们会获得一样的结果,也就是说,这两棵树在这个算法下发生了collision。而根据咱们以前提到的说法,这说明咱们的遍历算法是一个有损压缩的算法,一棵树在进行一次遍历时,其信息并不会完整地保留下来。那么到底是什么信息被丢弃了呢?咱们会发现,当一棵二叉树被按照“中-左-右”的方式遍历时,遇到最大的问题是当算法遇到一个并不存在的左子树或右子树时,算法自己并不会记录这种不存在的情况,而是选择忽略。而这样的信息倒是包含在一棵树的结构中的。也就是说,这样的“忽略”正是致使信息丢失的关键。这样,咱们就能够用一种新的遍历算法,只要标出忽略的位置(此处用·字符表示)就能够保存全部的信息。(请各位自行脑补算法)学习

这样,若是运行程序,那么咱们就会获得它的前序遍历结果:ABDG··H··E··C·FI···。而这个结果是和原二叉树一一对应的。

在这个例子中,新的遍历算法产生的结果有19个字符,比原来的字符数多了一倍还多。若是不考虑还有更简洁的算法,咱们就能够说原来的那种压缩算法实际上减小了整棵树所包含的近一半的信息量。而咱们想要找回以前的算法所丢失的信息,也须要以更多的字符数(物理空间)为代价。

可是在这里不少同窗都会想到一个反例,那就是逆波兰表达式。这是一种将操做数(operand)放在前方而将操做符(operator)放在操做数以后的表示方法。好比算术中缀表达式3 + 4就能够表示成3 4 +(是否是想起了Scheme:-)),这种表达式的优美之处就在于,它能够很方便的在计算机中经过栈实现计算,而对于人类来讲,这种表达方式的简洁则在于它能够彻底不用括号。好比中缀表达式(3 - 4) * 5能够写为3 4 - 5 *,而3 - 4 * 5则会写为3 4 5 * -。这样咱们彷佛又遇到一个难题,对于中缀表达式而言,每一位信息都是必须的(包括括号),可是为何却可以在物理字符数减小的状况下实现语义上面的等价转换呢?

实际上,这里确实并无发生神奇的信息量不变,而是有变化的。在进行逆波兰表达式的计算中,咱们会发现逆波兰表达式忽略了运算符的优先级这一律念,而是强制使用“从左向右”这种方式进行解析。而这种“从左向右”解析信息的规则,正是逆波兰表式在暗中所添加的信息。也就是说,表面上看逆波兰表达式减小了信息量,可实际上却增长了新的规定解析规则的信息使之避免了优先级的问题。一样对于中缀表达式,咱们也能够作这样的规定:无论符号的优先级,直接从左向右进行计算。这样,咱们的中缀表达式也就没有了用括号的必要。

可是这样的表示法的缺点也是显而易见的,由于3 - (4 * 5)彷佛并无办法在这种新规定下面表示出来,而在逆波兰表达式中却能够,因此一样的物理位彷佛仍包含了不一样的信息量。可是咱们若是再去仔细看一看逆波兰表达式就会发现,他们所包含的信息位并非相同的。在中缀表达式中,13 + 14能够直接表示为13+14,占用5个字符;而在逆波兰表达式中,咱们却不能够简单的写为1314+,由于咱们没法判断前两个操做数的分界线在哪里,有多是1+314也多是131+4。因此要想区分开两个操做数,咱们必须在之间加上一个空格,写做13 14+,这样咱们就须要6个字符来表示逆波兰表达式了。

C. 小结

如今咱们就明白,在逆波兰表达式表示法中,虽然经过强制的更“简单”的运算顺序规定使得括号消失,却会同时制造新的混乱。而在树的遍历中,虽然表面上保存了一棵树的全部信息,可是其中的隐藏信息,好比这棵树的左节点是否存在等却没有在某种特定的遍历中获得体现。咱们彷佛看到了隐约的守恒,虽然咱们到目前为止还没法量化。因此咱们能够大胆猜想,即便某种表示方式会兼顾逆波兰表示法的无括号的简洁与中缀表达式的清晰或者某种遍历表示会用相同的字符数来完整的还原出一棵树,咱们也没必要过度兴奋于它的精巧,由于这样就势必会有更加复杂的解析规则。其中包含的信息量虽然也许不会复杂到和原来相等,但至少会没有咱们理想情况下的那么好。

2. 信息冗余与疑虑

A. What makes a binary tree?

在咱们学习数据结构的时候,咱们必然会接触一个定理,就是给定一棵树的前序遍历和中序遍历或给定中序遍历和后序遍历便可还原出整棵树。有些老师还会提到若是只是给出前序遍历和后续遍历则不能够。在这里咱们能够作一个小实验,若是给出前序遍历“ABDE”与后序遍历“DBEA”,咱们能够来试着还原一下。还原后咱们会发现,这棵树能够有两种合法状况:分别是A(B(D()())())(E()())

ABDE

和A(B()(D()()))(E()())。

ABDE1

一样是两种不一样的遍历顺序,为何前序遍历却没法彻底和后序遍历相补充,而为何对于前序遍历和后序遍历,有的却又能够有惟一肯定的值呢?

经过咱们以前关于信息论的简单介绍,你们也应该能猜个大概。前序遍历和中序遍历、中序遍历和后序遍历之因此可以还原成惟一的二叉树,说明他们包含的信息已经足以覆盖这棵二叉树的所有信息。那么为何前序遍历和后序遍历却不能够呢?这一点其实也比较好理解,有线性代数基础的同窗确定会熟悉这样一种现象,即列向量a与列向量b线性无关(即a、b之间没法经过线性变换互相表示),向量b与向量c线性无关,可是咱们不能说明a与c也是线性无关的,他们之间不知足传递性。在a与c线性相关的状况下,a与c虽然并不相同,可是他们携带的信息却有不少重复。这样就形成了对于a、b、c三个不一样的向量,其实他们所携带的信息只须要a、b两个向量以及一个其余的小于c所携带的信息的“量”就能够了,那个“量”也就是a、c之间不重复的部分。

咱们还能够用集合来更通俗的解释一下,card(A)表示集合A的势(即A中元素的个数),咱们知道有一个公式: 。之因此存在小于的状况,就是由于集合A中与B中有重复的元素,因此根据集合的单一性,重复的元素被只保留一份。

其实前序遍历和后序遍历不能组成一棵完整的二叉树的缘由也在于此。若是咱们试图去根据前序、中序以及中序、后序遍历的方式去还原一棵二叉树的时候,咱们会发现每一位的数据都是有用的(你们的做业已经练得够多了吧)。之前序、中序为例,咱们先用前序最前面的元素肯定根节点,再到中序中找到,以此肯定了左右子树的范围,以后对子树递归调用此过程。在这个过程当中,因为本文第一节提到的左叶右叶可能为空,用找到根节点把整个子序列分为三段(左边为一段,节点自己为一段,右边为一段)的方法,就能够很清楚的肯定左边、右边分别有哪些元素。而对于前序、后序遍历,咱们在执行第一步的时候就已经会发现它的破绽了。但咱们查看前序遍历的第一位时,它对应表明着整棵树的根节点。可是若是咱们据此去看后序遍历的最后一个节点时,会发现它也必然会是整棵树的根节点。也就是说,咱们在这个过程当中并不存在“找”的过程,由于后序遍历的最后一个节点已经和前序遍历的第一个节点相同了。这样,虽然这三种遍历组合方式有着一样的字符量,可是由于前序、后序遍历存在着信息冗余(即信息上的重复),因此他们包含的有用信息其实并无那么多。这样,若是前序、中序遍历刚好可以等价于一颗完整的二叉树,其中的信息很少也很多,那么相对其少一些信息量的前序、后序遍历,天然也就没法包含完整的二叉树信息了。

B. 仍有疑虑的想法

可是这种解释并非完备的。由于咱们能够发现并非全部的前序、后序遍历均不能生成惟一的二叉树。好比前序遍历为ABCDE,后序遍历为CDBEA的树有且仅有一个:A(B(C()())(D()()))(E()())。要求这棵树的遍历还原问题实际上就能够简化为求前序为BCD后序为CDB的树的还原。咱们稍微分析一下就会发现它的树是惟一的,由于C肯定了左子树的结束位置,而这个左子树的元素个数刚好是一个,这就肯定了这棵树的惟一性。若是咱们把后序改成DCB,那么结果将大为不一样,咱们的树就又恢复到了不肯定的状态。

至此,咱们知道即便在前序、后序遍历中损失了一些信息,咱们仍然能够在一些特殊的状况中获得完整的树。这就是说,也许在前序中序,或者中序后序遍历中,信息也存在着必定的冗余。只不过这种冗余以一种不易察觉的方式存在。固然也许还有一种可能,那就是用这种想法来理解二叉树一开始就不靠谱。这倒不是信息论自己的问题,而是信息是否以在这片文章我猜想的方式存在着信息量与物理上面的严格对应。由于我原本就没有信息论的基础,屡次google后也没有找到满意的答案,因此只能做此颇有限的猜想和推理。

C.小结

其实以前一直没有想到会遇到最后的这个问题,可是遇到以后才发现也许信息原本就是一种复杂的东西。就拿咱们的某个遍历产生的字符串来讲,它自身的信息就是各个字母的排列,可是这些排列之中却有着太多的含义。有不少时候我觉得本身已经从单一的字符串中挖掘出了足够的信息,可是当有另外一个字符串、另外一个遍历顺序时,我发现它们之间的共同点和差别却体如今我原来并无考虑到的那一部分中。信息论看似诱人,彷佛揭示这某种本质性的东西,可是咱们怎么可以保证咱们已经对一段信息有着彻底的把握,这种已经把握的本质之下没有存在这更深入的本质?而在关于最后信息和物理位之间的关系,我一直没有找到合适的资料,也许是个人查找范围不够,也许是之前就有人意识到这是个很复杂的问题。但不管如何,咱们都已经经历了以一个新的视角来看待这种很基本的数据结构的过程,而我本身在学习过程当中,也一直喜欢以更宏观的视角来把握,这样的里外融合的感受也是很美好的。

后记

这篇文章虽然拖了很长时间才写完,我也从中收获了不少。由于我原本没有信息论的基础,在写这篇文章前只是对信息论有个很模糊的印象。然而为了逼迫本身“从无知到有知”,我仍是以为要写出这么一篇文章来,以增强我对信息论的了解。事实上也是如此,在写这篇文章的过程当中,我翻阅了吴军先生的《数学之美》,可是感受不够,又参考了一些MacKay的《Information Theory, Inference and Learning Algorithms》,看了几年前TopLang上面的讨论贴,又查阅了不少次google和wiki。虽然最后都没有解决我最后的疑问(也许是我看的不仔细),可是确实学到了不少,也顺便完成了个人第一个js小程序。并且在写这篇文章的过程当中,新的想法不断涌现,因此不得不“增删文稿数次”,也印证了pongba所说的“书写的时候,新的内容仍然源源不断的冒出来”

相关文章
相关标签/搜索