最近在學習java的數據結構與算法知識,看到數據結構 樹的遍歷的方式。在理解過程當中。查看到一篇文章,視野很是有深度,在信息論的角度看待這個問題。在此貼出該文章的連接以及內容。php
【文章出處】http://www.binarythink.net/2012/12/binary-tree-info-theory/ java
咱們在學習二叉樹的遍歷時,都會不可避免的學到二叉樹的三種遍歷方式,分別是遵循(根-左-右)的前序遍歷、遵循(左-根-右)的中序遍歷以及遵循(左-右-根)的後序遍歷。而且每個二叉樹均可以用這三種遍歷方式而且分別轉換爲字符串序列,以便在計算機上面保存。可是咱們在進行逆向操做的時候卻會遇到困難:咱們並不能從某一種遍歷方式推斷出惟一的二叉樹,也就是說,這是個單向編碼的過程。而當咱們有一個二叉樹的兩種遍歷方式的表述時,彷佛也不能作到盡善盡美:前序遍歷和中序遍歷的組合或者中序遍歷和後序遍歷的組合能夠逆向生成惟一的二叉樹,可是恰恰前序遍歷和後續遍歷卻不能夠。這其中的緣由是什麼呢?算法
在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位的字符串經過哈希函數處理後,實質上是完成了一次信息壓縮。而當處理以後的信息再解碼時不能生成惟一的信息時,咱們就發現這種壓縮是有損的。由於當一個肯定信息經過某種處理使它的不肯定性增大時,其包含的信息量也就會減小。安全
到如今咱們就會忽然發現,其實對於一棵樹的遍歷也能夠看作一次哈希映射。只不過此次咱們是將一個具體的有特定結構的樹映射爲長度爲樹的節點個數的字符串,而咱們的哈希函數就是同窗們所熟悉的遍歷順序。好比咱們能夠構造這樣一棵樹A(B(D(G()())(H()()))(E()()))(C()(F(I()())())) (這種繁瑣的二叉樹表示法可參考這裏)數據結構
其經過前序遍歷這一哈希函數的處理,咱們就能夠獲得這個字符串:ABDGHECFI。然而,當咱們將這個前序遍歷的字符串進行解碼時,咱們卻發現沒法結果並非惟一的。好比,對於二叉樹A(B(D(G()())(H()()))(E()()))(C()(F()(I()())))函數
當咱們經過前序遍歷進行哈希映射時,咱們會獲得一樣的結果,也就是說,這兩棵樹在這個算法下發生了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個字符來表示逆波蘭表達式了。
如今咱們就明白,在逆波蘭表達式表示法中,雖然經過強制的更「簡單」的運算順序規定使得括號消失,卻會同時製造新的混亂。而在樹的遍歷中,雖然表面上保存了一棵樹的全部信息,可是其中的隱藏信息,好比這棵樹的左節點是否存在等卻沒有在某種特定的遍歷中獲得體現。咱們彷佛看到了隱約的守恆,雖然咱們到目前爲止還沒法量化。因此咱們能夠大膽猜想,即便某種表示方式會兼顧逆波蘭表示法的無括號的簡潔與中綴表達式的清晰或者某種遍歷表示會用相同的字符數來完整的還原出一棵樹,咱們也沒必要過度興奮於它的精巧,由於這樣就勢必會有更加複雜的解析規則。其中包含的信息量雖然也許不會複雜到和原來相等,但至少會沒有咱們理想情況下的那麼好。
在咱們學習數據結構的時候,咱們必然會接觸一個定理,就是給定一棵樹的前序遍歷和中序遍歷或給定中序遍歷和後序遍歷便可還原出整棵樹。有些老師還會提到若是隻是給出前序遍歷和後續遍歷則不能夠。在這裏咱們能夠作一個小實驗,若是給出前序遍歷「ABDE」與後序遍歷「DBEA」,咱們能夠來試着還原一下。還原後咱們會發現,這棵樹能夠有兩種合法狀況:分別是A(B(D()())())(E()())
和A(B()(D()()))(E()())。
一樣是兩種不一樣的遍歷順序,爲何前序遍歷卻沒法徹底和後序遍歷相補充,而爲何對於前序遍歷和後序遍歷,有的卻又能夠有惟一肯定的值呢?
經過咱們以前關於信息論的簡單介紹,你們也應該能猜個大概。前序遍歷和中序遍歷、中序遍歷和後序遍歷之因此可以還原成惟一的二叉樹,說明他們包含的信息已經足以覆蓋這棵二叉樹的所有信息。那麼爲何前序遍歷和後序遍歷卻不能夠呢?這一點其實也比較好理解,有線性代數基礎的同窗確定會熟悉這樣一種現象,即列向量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中有重複的元素,因此根據集合的單一性,重複的元素被只保留一份。
其實前序遍歷和後序遍歷不能組成一棵完整的二叉樹的緣由也在於此。若是咱們試圖去根據前序、中序以及中序、後序遍歷的方式去還原一棵二叉樹的時候,咱們會發現每一位的數據都是有用的(你們的做業已經練得夠多了吧)。之前序、中序爲例,咱們先用前序最前面的元素肯定根節點,再到中序中找到,以此肯定了左右子樹的範圍,以後對子樹遞歸調用此過程。在這個過程當中,因爲本文第一節提到的左葉右葉可能爲空,用找到根節點把整個子序列分爲三段(左邊爲一段,節點自己爲一段,右邊爲一段)的方法,就能夠很清楚的肯定左邊、右邊分別有哪些元素。而對於前序、後序遍歷,咱們在執行第一步的時候就已經會發現它的破綻了。但咱們查看前序遍歷的第一位時,它對應表明着整棵樹的根節點。可是若是咱們據此去看後序遍歷的最後一個節點時,會發現它也必然會是整棵樹的根節點。也就是說,咱們在這個過程當中並不存在「找」的過程,由於後序遍歷的最後一個節點已經和前序遍歷的第一個節點相同了。這樣,雖然這三種遍歷組合方式有着一樣的字符量,可是由於前序、後序遍歷存在着信息冗餘(即信息上的重複),因此他們包含的有用信息其實並無那麼多。這樣,若是前序、中序遍歷剛好可以等價於一顆完整的二叉樹,其中的信息很少也很多,那麼相對其少一些信息量的前序、後序遍歷,天然也就沒法包含完整的二叉樹信息了。
可是這種解釋並非完備的。由於咱們能夠發現並非全部的前序、後序遍歷均不能生成惟一的二叉樹。好比前序遍歷爲ABCDE,後序遍歷爲CDBEA的樹有且僅有一個:A(B(C()())(D()()))(E()())。要求這棵樹的遍歷還原問題實際上就能夠簡化爲求前序爲BCD後序爲CDB的樹的還原。咱們稍微分析一下就會發現它的樹是惟一的,由於C肯定了左子樹的結束位置,而這個左子樹的元素個數剛好是一個,這就肯定了這棵樹的惟一性。若是咱們把後序改成DCB,那麼結果將大爲不一樣,咱們的樹就又恢復到了不肯定的狀態。
至此,咱們知道即便在前序、後序遍歷中損失了一些信息,咱們仍然能夠在一些特殊的狀況中獲得完整的樹。這就是說,也許在前序中序,或者中序後序遍歷中,信息也存在着必定的冗餘。只不過這種冗餘以一種不易察覺的方式存在。固然也許還有一種可能,那就是用這種想法來理解二叉樹一開始就不靠譜。這倒不是信息論自己的問題,而是信息是否以在這片文章我猜想的方式存在着信息量與物理上面的嚴格對應。由於我原本就沒有信息論的基礎,屢次google後也沒有找到滿意的答案,因此只能做此頗有限的猜想和推理。
其實以前一直沒有想到會遇到最後的這個問題,可是遇到以後才發現也許信息原本就是一種複雜的東西。就拿咱們的某個遍歷產生的字符串來講,它自身的信息就是各個字母的排列,可是這些排列之中卻有着太多的含義。有不少時候我覺得本身已經從單一的字符串中挖掘出了足夠的信息,可是當有另外一個字符串、另外一個遍歷順序時,我發現它們之間的共同點和差別卻體如今我原來並無考慮到的那一部分中。信息論看似誘人,彷佛揭示這某種本質性的東西,可是咱們怎麼可以保證咱們已經對一段信息有着徹底的把握,這種已經把握的本質之下沒有存在這更深入的本質?而在關於最後信息和物理位之間的關係,我一直沒有找到合適的資料,也許是個人查找範圍不夠,也許是之前就有人意識到這是個很複雜的問題。但不管如何,咱們都已經經歷了以一個新的視角來看待這種很基本的數據結構的過程,而我本身在學習過程當中,也一直喜歡以更宏觀的視角來把握,這樣的裏外融合的感受也是很美好的。
這篇文章雖然拖了很長時間才寫完,我也從中收穫了不少。由於我原本沒有信息論的基礎,在寫這篇文章前只是對信息論有個很模糊的印象。然而爲了逼迫本身「從無知到有知」,我仍是以爲要寫出這麼一篇文章來,以增強我對信息論的瞭解。事實上也是如此,在寫這篇文章的過程當中,我翻閱了吳軍先生的《數學之美》,可是感受不夠,又參考了一些MacKay的《Information Theory, Inference and Learning Algorithms》,看了幾年前TopLang上面的討論貼,又查閱了不少次google和wiki。雖然最後都沒有解決我最後的疑問(也許是我看的不仔細),可是確實學到了不少,也順便完成了個人第一個js小程序。並且在寫這篇文章的過程當中,新的想法不斷涌現,因此不得不「增刪文稿數次」,也印證了pongba所說的「書寫的時候,新的內容仍然源源不斷的冒出來」。