2010年一部電影創造了奇蹟,它是全球第一部票房到達27億美圓。總票房歷史排名第一的影片,那就是詹姆斯.卡梅隆執導的電影《阿凡達》(Avatar)。算法
電影裏提到了一棵高達900英尺(約274米)的參天巨樹,是那個潘多拉星球的那威人的家園,讓人印象很是深入,惋惜那只是導演的夢想,地球上不存在這樣的物種。編程
不管多高多大的樹,那也是從小到大。由根到葉、一點點成長起來的。俗話說十年樹木、百年樹人,可一棵大樹又何止是十年這樣容易--哈哈,說到哪裏去了,咱們如今不是在上生物課,而是要講一種新的數據結構--樹。數組
以前咱們一直在談的是一對一的線性結構,可現實中,還有不少一對多的狀況須要處理,因此咱們須要研究這種一對多的數據結構--"樹",考慮它的各類特性,來解決咱們在編程中碰到的相關問題。 網絡
樹的定義其實就是咱們在講解棧時提到的遞歸的方法。也就是在樹的定義之中還用到了樹的概念,這是一種比較新的定義方法。圖6-2-2的子樹T1和子樹T2就是根結點A的子樹。固然,D、G、H、I組成的樹又是B爲結點的子樹,E、J組成的樹是C爲結點的子樹。 數據結構
對於樹的定義還須要強調兩點:數據結構和算法
樹的結點包含一個數據元素及若干個指向其子樹的分支。**結點擁有的子樹數稱爲結點的度(Degree)。度爲0的結點稱爲葉結點(Leaf)或終端結點;度不爲0的結點稱爲非終端結點或分支結點。除根結點以外,分支結點也稱爲內部結點。樹的度是樹內各結點的度的最大值。**如圖6-2-4所示,由於這棵樹結點的度的最大值是結點D的度,爲3,因此樹的度也爲3。 函數
結點的子樹的根稱爲該結點的孩子(Child)。相應地,該結點稱爲孩子的雙親(Parent)。嗯,爲何不是父或母,叫雙親呢?呵呵,對於結點來講其父母同體,惟一的一個,因此只能把它稱爲雙親了。同一個雙親的孩子之間互稱兄弟(Sibling)。結點的祖先是從根到該結點所經分支上的全部結點。因此對於H來講,D、B、A都是它的祖先。反之,以某結點爲跟的子樹的任一結點都稱爲該結點的子孫。B的子孫有D、G、H、I,如圖6-2-5所示。 性能
結點的層次(Level)從根開始定義起,根爲第一層,根的孩子爲第二層。若某結點在第l層,則其子樹的根就在第l+1層。其雙親在同一層的結點互爲堂兄弟。顯然圖6-2-6中的D、E、F是堂兄弟,而G、H、I、J也是。樹中結點的最大層次稱爲樹的深度(Depth)或高度,當前樹的深度爲4。 學習
若是將樹中結點的各子樹當作從左到右是有次序的,不能互換的,則稱該樹爲有序樹,不然稱爲無序樹。優化
森林(Forest)是m(m>=0)棵互不相交的樹的集合。對樹中每一個結點而言,其子樹的集合即爲森林。對於圖6-2-1中的樹而言,圖6-2-2中的兩棵子樹其實就能夠理解爲森林。
對比線性表與樹的結構,它們有很大的不一樣,如圖6-2-7所示。
相對於線性結構,樹的操做就徹底不一樣了,這裏咱們給出一些基本和經常使用操做。
說到存儲結構,就會想到咱們前面章節講過的順序存儲和鏈式存儲兩種結構。
先來看看順序存儲結構,用一段地址連續的存儲單元依次存儲線性表的數據元素。這對於線性表來講是很天然的,對於樹這樣一對多的結構呢?
樹中某個結點的孩子能夠有多個,這就意味着,不管按何種順序將樹中全部結點存儲到數組中,結點的存儲位置都沒法直接反映邏輯關係,你想一想看,數據元素挨個的存儲,誰是誰的雙親,誰是誰的孩子呢?簡單的順序存儲結構是不能知足樹的實現要求的。
不過充分利用順序存儲和鏈式存儲結構的特色,徹底能夠實現對樹的存儲結構的表示。咱們這裏要介紹三種不一樣的表示法:雙親表示法、孩子表示法、孩子兄弟表示法。
咱們人可能由於種種緣由,沒有孩子,但不管是誰都不可能從石頭蹦出來的,孫悟空顯然不能算是人,因此是人必定會有父母。樹這種結構也不例外,除了根結點外,其他每一個結點,它不必定有孩子,可是必定有且僅有一個雙親。
咱們假設以一組連續空間存儲樹的結點,同時在每一個結點中,附設一個指示器指示其雙親結點到鏈表中的位置。也就是說,每一個結點除了知道本身是誰之外,還知道它的雙親在哪裏。它的結點結構爲表6-4-1所示。
其中data是數據域,存儲結點的數據信息。而parent是指針域,存儲該結點的雙親在數組中的下標。
如下是咱們的雙親表示法的結點結構定義代碼。
有了這樣的結構定義,咱們就能夠來實現雙親表示法了。因爲根結點是沒有雙親的,因此咱們約定根結點的位置域設置爲-1,這也就意味着,咱們全部的結點都存有它雙親的位置。如圖6-4-1中的樹結構和表6-4-2中的樹雙親表示所示。
這樣的存儲結構,咱們能夠根據結點的parent指針很容易找到它的雙親結點,所用的時間複雜度爲O(1),直到parent爲-1時,表示找到了樹結點的根。可若是咱們要知道結點的孩子是什麼,對不起,請遍歷整個結構才行。
這真是麻煩,能不能改進一下呢?
固然能夠。咱們增長一個結點最左邊孩子的域,不妨叫它長子域,這樣就能夠很容易獲得結點的孩子。若是沒有孩子的結點,這個長子域就設置爲-1,如表6-4-3所示。
對於有0個或1個孩子結點來講,這樣的結構是解決了要找結點孩子的問題了。甚至是有2個孩子,知道了長子是誰,另外一個固然就是次子了。
另一個問題場景,咱們很關注各兄弟之間的關係,雙親表示法沒法體現這樣的關係,那咱們怎麼辦?嗯,能夠增長一個右兄弟域來體現兄弟關係,也就是說,每個結點若是它存在右兄弟,則記錄下右兄弟的下標。一樣的,若是右兄弟不存在,則賦值爲-1,如表6-4-4所示。
但若是結點的孩子不少,超過了2個。咱們又關注結點的雙親、又關注結點的孩子、還關注結點的兄弟,並且對時間遍歷要求還比較高,那麼咱們還能夠把次結構擴展爲有雙親域、長子域、再有右兄弟域。存儲結構的設計是一個很是靈活的過程。一個存儲結構設計得是否合理,取決於基於該存儲結構的運算是否適合,是否方便,時間複雜度好很差等。注意也不是越多越好,有須要時再設計相應的結構。就像再好聽的音樂,不停 反覆聽上千遍也會膩味,再好看的電影,一段時間反覆看上百遍,也會無趣,大家說是吧。
換一種徹底不一樣的考慮方法。因爲樹中每一個結點可能有多棵子樹,能夠考慮用多重鏈表,即每一個結點有多個指針域,其中每一個指針指向一棵子樹的根結點,咱們把這種方法叫作多重鏈表表示法。不過,樹的每一個結點的度,也就是它的孩子個數是不一樣的。因此能夠設計兩種方案來解決。
一種是指針域的個數就等於樹的度,複習一下,樹的度是樹各個結點度的最大值。其結構如表6-4-5所示。
其中data是數據域。child1到childd是指針域,用來指向該結點的孩子結點。
對於圖6-4-1的樹來講,樹的度是3,因此咱們的指針域的個數是3,這種方法實現如圖6-4-2所示。
這種方法對於樹中各結點的度相差很大時顯然是很浪費空間的,由於有不少的結點,它的指針域都是空的。不過若是樹的各結點度相差很小時,那就意味着開闢的空間被充分利用了,這時存儲結構的缺點反而變成了優勢。
既然不少指針域均可能爲空,爲何不按需分配空間呢。因而咱們有了第二種方案。
第二種方案每一個結點指針域的個數等於該結點的度,咱們專門取一個位置來存儲結點指針域的個數,其結構如表6-4-6所示。
其中data爲數據域,degree爲度域,也就是存儲該結點的孩子結點的個數,child1到childd爲指針域,指向該結點的各個孩子的結點。
對於圖6-4-2的樹來講,這種方法實現如圖6-4-3所示。
這種方式克服了浪費空間的缺點,對空間利用率是很高了,可是因爲各個結點的鏈表是不相同的結構,加上要維護結點的度的數值,在運算上就會帶來時間上的損耗。
可否有更好的方法,既能夠減小空指針的浪費又能使結點結構相同。
仔細觀察,咱們爲了要遍歷整棵樹,把每一個結點放到一個順序存儲結構的數組中是合理的,但每一個結點的孩子有多少是不肯定的,因此咱們再對每一個結點的孩子創建一個單鏈表體現它們的關係。
這就是咱們要講的孩子表示法。具體辦法是,把每一個結點的孩子結點排列起來,以單鏈表做存儲結構,則n個結點由n個孩子鏈表,若是是葉子結點則此單鏈表爲空。而後n個頭指針又組成一個線性表,採用順序存儲結構,存放進一個一維數組中,如圖6-4-4所示。
爲此,設計兩種結點結構,一個是孩子鏈表的孩子結點,如表6-4-7所示。
其中child是數據域,用來存儲某個結點在表頭數組中的下標。next是指針域,用來存儲指向某結點的下一個孩子結點的指針。
另外一個是表頭數組的表頭結點,如表6-4-8所示。
其中data是數據域,存儲某結點的數據信息。firstchild是頭指針域,存儲該結點的孩子鏈表的頭指針。
如下是咱們的孩子表示法的結構定義代碼。
這樣的結構對於咱們要查找某個結點的某個孩子,或者找某個結點的兄弟,只須要查找這個結點的孩子單鏈表便可。對於遍歷整棵樹也是很方便的,對頭結點的數組循環便可。
可是,這也存在着問題,我如何知道某個結點的雙親是誰呢?比較麻煩,須要整棵樹遍歷才行,難道就不能夠把雙親表示法和孩子表示法綜合一下嗎?固然是能夠。如圖6-4-5所示。
咱們把這種方法稱爲雙親孩子表示法,應該算是孩子表示法的改進。至於這個表示法的具體結構定義,這裏就略過,留給同窗們本身去設計了。
剛纔咱們分別從雙親的角度和從孩子的角度研究樹的存儲結構,若是咱們從樹結點的兄弟的角度又會如何呢?固然,對於樹這樣的層級結構來講,只研究結點的兄弟是不行的,咱們觀察後發現,任意一棵樹,它的結點的第一個孩子若是存在就是惟一的,它的右兄弟若是存在也是惟一的。所以,咱們設置兩個指針,分別指向該結點的第一個孩子和此結點的右兄弟。
結點結構如表6-4-9所示。
其中data是數據域,firstchild爲指針域,存儲該結點的第一個孩子結點的存儲地址,rightsib是指針域,存儲該結點的右兄弟結點的存儲地址。
結構定義代碼以下。
對於圖6-4-1的樹來講,這種方法實現的示意圖如圖6-4-6所示。
這種表示法,給查找某個結點的某個孩子帶來了方便,只須要經過fistchild找到此結點的長子,而後再經過長子結點的rightsib找到它的二弟,直到找到具體的孩子。固然,若是想找某個結點的雙親,這個表示法也是有缺陷的,那怎麼辦呢?
呵呵,對,若是真的有必要,徹底能夠再增長一個parant指針域來解決快速查找雙親的問題,這裏就再也不細談了。
其實這個表示法的最大好處是它把一棵複雜的樹變成了一棵二叉樹。咱們把圖6-4-6變變形就成了圖6-4-7這個樣子。
這樣就能夠充分利用二叉樹的特性和算法來處理這棵樹了。嗯?有人問,二叉樹是什麼?哈哈,別急,這正是我接下來要重點講的內容。
如今咱們來作個遊戲,我在紙上已經寫好了一個100之內的正整數數字,請你們想辦法猜出我寫的是哪個?注意大家猜的數字不能超過7個,個人回答只會告訴你是"大了"或"小了"。
這個遊戲在一些電視節目中,猜想一些商品的訂價時常會使用。我看到過有些人是一點一點的數字累加的,好比5,10,15,20這樣猜,這樣的猜數策略過低級了,顯然是沒有學過數據結構和算法的人才作得出的事。
其實這個一個很經典的折半查找算法。若是咱們用圖6-5-1(下三層省略)的辦法,就必定能在7次之內,猜出結果來。
因爲是100之內的正整數,因此咱們先猜50(100的一半),被告知"大了",因而再猜25(50的一半),被告知"小了",再猜37(25與50的中間數),小了,因而猜43,大了,40大了,38小了,39徹底正確。過程如表6-5-1所示。
咱們發現,若是用這種方式進行查找,效率搞得不是一點點。對於折半查找的詳細講解,咱們後面章節再說。不過對於這種在某個階段都是兩種結果的情形,好比開和關,0和1,真和假,上和下,對與錯,正面與反面等,都適合用樹狀結構來建模,而這種樹是一種很特殊的樹狀結構,叫作二叉樹。
圖6-5-2就是一棵二叉樹。而圖6-2-1的樹,由於D結點有三個子樹,因此它不是二叉樹。
二叉樹的特色有:
二叉樹具備五種基本狀態:
應該說這五種形態仍是比較好理解的,那我如今問你們,若是是有三個結點的樹,有幾種形態?若是是有三個結點的二叉樹,考慮一下,又有幾種形態?
若只從形態上考慮,三個結點的樹只有兩種狀況,那就是圖6-5-4中有兩層的樹1和有三層的後四種的任意一種,但對於二叉樹來講,因爲要區分左右,因此就演變成五種形態,樹2,樹3,樹4和樹5分別表明不一樣的二叉樹。
咱們再來介紹一些特殊的二叉樹。這些樹可能暫時你不能理解它有什麼用處,但先了解一下,之後會提到它們的實際用途。
顧名思義,斜樹必定要是斜的,可是往哪斜仍是有講究。全部的結點都只有左子樹的二叉樹叫左斜樹,全部結點都只有右子樹的二叉樹叫右斜樹。這二者統稱爲斜樹。圖6-5-4中的樹2就是左斜樹,樹5就是右斜樹。斜樹有很明顯的特色,就是每一層都只有一個結點,結點的個數與二叉樹的深度相同。
有人會想,這也能叫樹呀,與咱們的線性表結構不是同樣嗎。對的,其實線性表結構就能夠理解爲是樹的一種極其特殊的表現形式。
蘇東坡曾有詞雲:"人有悲歡離合,月有陰晴圓缺,此時古難全"。意思就是完美是理想,不完美才是人生。咱們一般舉的例子也都是左高右低、良莠不齊的二叉樹。那是否存在完美的二叉樹呢?
嗯,有同窗已經在空中手指比劃起來。對的,完美的二叉樹是存在的。
在一棵二叉樹中,若是全部分支結點都存在左子樹和右子樹。而且全部葉子都在同一層上,這樣的二叉樹稱爲滿二叉樹。
圖6-5-5就是一棵滿二叉樹,從樣子上看就感受它很完美。
單是每一個結點都存在左右子樹,不能算是滿二叉樹,還必需要全部的葉子都在同一層上,這就作到了整棵樹的平衡。所以,滿二叉樹的特色有:
葉子只能出如今最下一層。出如今其餘層就不可能達到平衡。
非葉子結點的度必定是2.不然就是"缺胳膊少腿"了。
在一樣深度的二叉樹中,滿二叉樹的結點個數最多,葉子數最多。
徹底二叉樹
對一棵具備n個結點的二叉樹按層序編號,若是編號爲i(1<=i<=n)的結點與一樣深度的滿二叉樹中編號爲i的結點在二叉樹中位置徹底相同,則這課二叉樹稱爲徹底二叉樹,如圖6-5-6所示。
這是一種有些理解難度的特殊二叉樹。
首先從字面上要區分,"徹底"和"滿"的差別,滿二叉樹必定是一棵徹底二叉樹,但徹底二叉樹不必定是滿的。
其次,徹底二叉樹的全部結點與一樣深度的滿二叉樹,它們按層序編號相同的結點,是一一對應的。這裏有個關鍵詞是按層序編號,像圖6-5-7中的樹1,由於5結點沒有左子樹,卻有右子樹,那就是使得按層序編號的第10個編號空擋了。一樣道理,圖6-5-7中的樹3又是由於5編號下沒有子樹形成第10和11位置空擋。只有圖6-5-6中的樹,儘管它不是滿二叉樹,可是編號是連續的,因此它是徹底二叉樹。
從這裏我也能夠得出一些徹底二叉樹的特色:
從上面的例子。也給了咱們一個判斷某二叉樹是否徹底二叉樹的辦法,那就是看着樹的示意圖,心中默默給每一個結點按照滿二叉樹的結構逐層順序編號,若是編號出現空擋,就說明不是徹底二叉樹,不然就是。
二叉樹有一些須要理解並記住的特性,以便於咱們更好地使用它。
性質1:在二叉樹的第i層上至多有2(i-1)個結點(i>=1)。 這個性質很好記憶,觀察一下圖6-5-5.
經過數據概括法的論證,能夠很容易得出在二叉樹的第i層上至多有2(i-1)個結點(i>=1)的結論。
性質2:深度爲k的二叉樹至多有2(k)-1個結點(k>=1)。
注意這裏必定要看清楚,是2(k)後再減去1,而不是2(k-1)。之前不少同窗不能徹底理解,這樣去記憶,就容易把性質2與性質1給弄混淆了。
深度爲k意思就是有k層的二叉樹,咱們先來看看簡單的。
經過數據概括法的論證,能夠得出,若是有k層,此二叉樹至多有2(k)-1個結點。
性質3:對任何一棵二叉樹T。若是其終端結點數爲n0,度爲2的結點數爲n2,則n0=n2+1。
終端結點數其實就是葉子結點數,而一棵二叉樹,除了葉子結點外,剩下的就是度爲1或2的結點數了,咱們設n1爲度是1的結點數。則樹T結點 總數n=n0+n1+n2。
好比圖6-6-1的例子,結點總數爲10,它是有A、B、C、D等度爲2結點,F、G、H、I、J等度爲0的葉子結點和E這個度爲1的結點組成。總和爲4+1+5=10。
咱們換個角度,再數一數它的鏈接線數,因爲根結點只有分支出去,沒有分支進入,因此分支總數爲結點總數減去1.圖6-6-1就是9個分支。對於A、B、C、D結點來講,它們都有兩個分支線出去,而E結點只有一個分支線出去。因此總分支線爲4x2+1x1=9。
用代數表達就是分支線總數=n-1=n1+2n2。由於剛纔咱們有等式n=n0+n1+n2,因此可推導出n0+n1+n2-1=n1+2n2。結論就是n0=n2+1。
性質4:具備n個結點的徹底二叉樹的深度爲[log2n]+1([x]表示不大於x的最大整數)。
由滿二叉樹的定義咱們能夠知道,深度爲k的滿二叉樹的結點數n必定是2(k)-1。由於這個最多的結點個數。那麼對於n=2(k)-1倒推獲得滿二叉樹的度數爲k=log2(n+1),好比結點數爲15的滿二叉樹,度爲4.
徹底二叉樹咱們前面已經提到,它是一棵具備n個結點的二叉樹,若按層序編號後其編號與一樣深度的滿二叉樹中編號結點在二叉樹位置徹底相同,那它就是徹底二叉樹。也就是說,它的葉子結點只會出如今最下面的兩層。
它的結點數必定少於等於一樣度數的滿二叉樹的結點數2(k)-1,但必定多於2(k-1)-1。即知足2(k-1)-1<n<=2(k)-1。因爲結點數n是整數,n<=2(k)-1意味着n<2(k),n>2(k-1)-1,意味着n>=2(k-1),因此2(k-1)<=n<2(k),不等式兩邊取對數,獲得k-1<=log2n<k,而k做爲度數也是整數,所以k=[log2n]+1。
性質5:若是對一棵有n個結點的徹底二叉樹(其深度爲[log2n]+1)的結點按層序編號(從第一層到第[log2n]+1層,每層從左到右),對任一結點i(1<=i<=n)有:
咱們以圖6-6-2爲例,來理解這個性質。這是一個徹底二叉樹,度爲4,結點總數是10。
前面咱們已經談到了樹的存儲結構,而且談到了順序存儲對樹這種一對多的關係結構實現起來是比較困難的。可是二叉樹是一種特殊的樹,因爲它的特殊性,使得用順序存儲結構也能夠實現。
二叉樹的順序存儲結構就是用一維數組存儲二叉樹中的結點,而且結點的存儲位置,也就是數組的下標要能體現結點之間的邏輯關係,好比雙親與孩子的關係,左右兄弟的關係等。
先來看看徹底二叉樹的順序存儲,一棵徹底二叉樹如圖6-7-1所示。
將這棵二叉樹存入到數組中,相應的下標對應其一樣的位置,如圖6-7-2所示。
這下看出徹底二叉樹的優越性來了吧。因爲它定義的嚴格,因此用順序結構也能夠表現出二叉樹的結構來。
固然對於通常的二叉樹,儘管層序編號不能反映邏輯關係,可是能夠將其按徹底二叉樹編號,只不過,把不存在的結點設置爲"^"而已。如圖6-7-3,注意淺色結點表示不存在。
考慮一種極端的狀況,一棵深度爲k的右斜樹,它只有k個結點,卻須要分配2(k)-1個存儲單元空間,這顯然是對存儲空間的浪費,例如圖6-7-4所示。因此,順序存儲結構通常只用於徹底二叉樹。
既然順序存儲適用性不強,咱們就要考慮鏈式存儲結構。二叉樹每一個結點最多有兩個孩子,因此爲它設計一個數據域和兩個指針域是比較天然的想法,咱們稱這樣的鏈表叫作二叉鏈表。結點結構圖如表6-7-1所示。
其中data是數據域,lchild和rchild都是指針域,分別存放指向左孩子和右孩子的指針。
如下是咱們的二叉鏈表的結點結構定義代碼。
結構示意圖如圖6-7-5所示。
就如同樹的存儲結構中討論的同樣,若是有須要,還能夠再增長一個指向其雙親的指針域,那樣就稱之爲二叉鏈表。因爲與樹的存儲結構相似,這裏就不詳述了。
假設,我手頭有20張100元的和2000張1元的獎券,同時灑向了空中,你們比賽看誰最終撿的最多。若是是你,你會怎麼作?
相信全部同窗都會說,必定先撿100元的。道理很是簡單,由於撿一張100元等於1元的撿100張,效率好得不是一點點。因此能夠獲得這樣的結論,一樣是賤獎券,在有限時間內,要達到最高效率,次序很是重要。對於二叉樹的遍從來講,次序一樣顯得很重要。
二叉樹的遍歷(traversing binary tree)是指從根結點出發,按照某種次序依次訪問二叉樹中全部結點,使得每一個結點被訪問一次且僅被訪問一次。
這裏有兩個關鍵詞:訪問和次序。
訪問實際上是要根據實際的須要來肯定具體作什麼,好比對每一個結點進行相關計算,輸出打印等,它算做一個抽象操做。在這裏咱們能夠簡單地假定就是輸出結點的數據信息。
二叉樹的遍歷次序不一樣於線性結構,最多也就是從頭至尾、循環、雙向等簡單的遍歷方式。樹的結點之間不存在惟一的前驅和後繼關係,在訪問一個結點後,下一個被訪問的結點面臨着不一樣的選擇。就像你人生的道路上,高考填志願要面臨哪一個城市、哪所大學、具體專業等選擇,因爲選擇方式的不一樣,遍歷的次序就徹底不一樣了。
二叉樹的遍歷方式能夠不少,若是咱們限制了從左到右的習慣方式,那麼主要就分爲四種:
規則是若二叉樹爲空,則空操做返回,不然先訪問根結點,而後前序遍歷左子樹,再前序遍歷右子樹。如圖6-8-2所示,遍歷的順序爲:ABDGHCEIF。
規則是若樹爲空,則空操做返回,不然從根結點開始(注意並非先訪問根結點),中序遍歷根結點的左子樹,而後是訪問根結點,最後中序遍歷右子樹。如圖6-8-3所示,遍歷的順序爲:GDHBAEICF。
規則是若樹爲空,則空操做返回,不然從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點。如圖6-8-4所示,遍歷的順序爲:GHDBIEFCA。
規則是若樹爲空,則空操做返回,不然從樹的第一層,也就是根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。如圖6-8-5所示,遍歷的順序爲:ABCDEFGHI。
有同窗會說,研究這麼多遍歷的方法幹什麼呢?
咱們用圖形的方式來表現樹的結構,應該說是很是直觀和容易理解,可是對於計算機來講,它只有循環、判斷等方式來處理,也就是說,它只會處理線性序列,而咱們剛纔提到的四種遍歷方法,其實都是在把樹中的結點變成某種意義的線性序列,這就給程序的實現帶來了好處。
另外不一樣的遍歷提供了對結點依次處理的不一樣方式,能夠在遍歷過程當中對結點進行各類處理。
二叉樹的定義是用遞歸的方式,因此,實現遍歷算法也能夠採用遞歸,並且極其簡潔明瞭。先來看看二叉樹的前序遍歷算法。代碼以下:
假設咱們如今有如圖6-8-6這樣一棵二叉樹T。這樹已經用二叉鏈表結構存儲在內存當中。
那麼當調用PreOrderTraverse(T)函數時,咱們來看看程序是如何運行的。
調用ProOrderTraverse(T),T根結點不爲null,因此執行printf,打印字母A,如圖6-8-7所示。
調用PreOrderTraverse(T->lchild);訪問了A結點的左孩子,不爲null,執行printf顯示字母B,如圖6-8-8所示。
此時再次遞歸調用PreOrderTraverse(T->lchild);訪問了B結點的左孩子,執行printf顯示字母D,如圖6-8-9所示。
再次遞歸調用調用ProOrderTraverse(T->lchild);訪問了D結點的左孩子,執行printf顯示字母H,如圖6-8-10所示。
再次遞歸調用ProOrderTraverse(T-lchild);訪問了H結點的左孩子,此時由於H結點無左孩子,因此T==null,返回此函數,此時遞歸調用ProOrderTraverse(T-rchild);訪問了H結點的右孩子,printf顯示字母K,如圖6-8-11所示。
再次遞歸調用調用ProOrderTraverse(T->lchild);訪問了K結點的左孩子,K結點無左孩子,返回,調用ProOrderTraverse(T->rchild);訪問了K結點的右孩子,也是null,返回。因而此函數執行完畢,返回到上一級遞歸的函數(即打印H結點時的函數),也執行完畢,返回到打印D時的函數,調用ProOrderTraverse(T->lchild);訪問了D結點的右孩子,不存在,返回到B結點,調用ProOrderTraverse(T->lchild);找到告終點E,打印字母E,如圖6-8-12所示。
因爲結點E沒有左右孩子,返回打印結點B時的遞歸函數,遞歸執行完畢,返回到最初的PreOrderTraverse,調用PreOrderTraverse(T->rchild);訪問結點A的右孩子,打印字母C,如圖6-8-13所示。
以後相似前面的遞歸調用,依次繼續打印F、I、G、J,步驟略。
綜上,前序遍歷這棵二叉樹的結點順序是:ABDHKECFIGJ。
那麼二叉樹的中序遍歷算法是如何呢?哈哈,別覺得很複雜,它和前序遍歷算法僅僅只是代碼的順序上的差別。
換句話說,它等因而把調用左孩子的遞歸函數提早了,就那麼簡單。咱們來看看當調用InOrderTraverse(T)函數時,程序是如何運行的。
調用InOrderTraverse(T),T的根節點不爲nulll,因而調用InOrderTraverse(T->lchild);訪問結點B。當前指針不爲null,繼續調用InOrderTraverse(T->lchild);訪問結點D。不爲null,繼續調用InOrderTraverse(T->lchild);訪問結點H。繼續調用InOrderTraverse(T->lchild);訪問結點H的左孩子,發現當前指針爲null,因而返回。打印當前結點H,如圖6-8-14所示。
而後調用InOrderTraverse(T->rchild);訪問結點H的右孩子K,因結點K無左孩子,因此打印K,如圖6-8-15所示。
由於結點K沒有右孩子,因此返回。打印結點H函數執行完畢,返回。打印字母D,如圖6-8-16所示。
結點D無右孩子,此函數執行完畢,返回。打印字母B,如圖6-8-17所示。
調用InOrderTravere(T->rchild);訪問結點B的右孩子E,因結點E無左孩子,因此打印E,如圖6-8-18所示。
結點E無右孩子,返回。結點B的遞歸函數執行完畢,返回到了最初咱們調用InOrderTraverse的地方,打印字母A,如圖6-8-19所示。
綜上,中序遍歷這棵二叉樹的結點順序是:HKDBEAIFCGJ。
那麼一樣的,後序遍歷也就很容易想到應該如何寫代碼了。
如圖6-8-20所示,後序遍歷是先遞歸左子樹,由根結點A->B->D->H,結點H無左孩子,再查看結點H的右孩子K,由於結點K無左右孩子,因此打印K,返回。
最終,後序遍歷的結點的順序就是:KHDEBIFJGCA。同窗們能夠本身按照剛纔的辦法得出這個結果。
有一種題目爲了考查你對二叉樹遍歷的掌握程度,是這樣出題的。已知一棵二叉樹的前序遍歷序列爲:ABCDEF,中序遍歷序列爲:CBAEDF,請問這棵二叉樹的後序遍歷結果是多少?
對於這樣的題目,若是真的徹底理解了前中後序的原理,是不難的。
三種遍歷都是從根結點開始,前序遍歷是先打印再遞歸左和右。因此前序遍歷序列爲ABCDEF。第一個字母是A被打印出來,就說明A是根結點的數據。再由中序遍歷序列是CBAEDF,能夠知道C和B是A的左子樹的結點,E、D、F是A的右子樹的結點,如圖6-8-21所示。
而後咱們看前序中的C和B,它的順序是ABCDEF,是先打印B後打印C,因此B應該是A的左孩子,而C就只能是B的孩子,此時是左仍是右孩子還不肯定。再看中序序列是CBAEDF,C是在B的前面打印,這就說明C是B的左孩子,不然就是右孩子了,如圖6-8-22所示。
再看前序中的E、D、F,它的順序是ABCDEF,那就意味着D是A結點的右孩子,E和F是D的子孫,注意,他們中有一個不必定是孩子,還有多是孫子的。再來看中序序列是CBAEDF,因爲E在D的左側,而F在右側,因此能夠肯定E是D的左孩子,F是D的右孩子。所以最終獲得的二叉樹是圖6-8-23所示。
爲了不推導中的失誤,你最好在心中遞歸遍歷,檢查一下這棵樹的前序和中序遍歷序列是否與題目中的相同。
已經復原了二叉樹,要得到它的後序遍歷結果就是易如反掌,結果是CBEFDA。
但其實,若是同窗們足夠熟練,不用畫這棵二叉樹,也能夠獲得後序的結果,由於剛纔判斷了A結點是根節點,那麼它在後序序列中,必定是最後一個。剛纔推導出C是B的左孩子,而B是A的左孩子,那就意味着後序序列的前兩位必定是CB。一樣的辦法也能夠獲得EFD這樣的後序順序,最終就天然獲得CBEFDA這樣的序列,不用在草稿上畫樹狀圖了。
反過來,若是咱們的題目是這樣:二叉樹的中序序列是ABCDEFG,後序序列是BDCAFGE,求前序序列。
此次簡單點,由後序的BDCAFGE,獲得E是根結點,所以前序首字母是E。
因而根據中序序列分爲兩棵樹ABCD和FG,由有序序列的BDCAFGE,知道A是E的左孩子,前序序列目前分析爲EA。
再由中序序列的ABCDEFG,知道BCD是A結點的右子孫,再由後序序列的BDCAFGE知道C結點是A結點的右孩子,前序序列目前分析獲得EAC。
中序序列ABCDEFG,獲得B是C的左孩子,D是C的右孩子,因此前序序列目前分析結果爲EACBD。
由後序序列BDCAFGE,獲得G是E的右孩子,因而F就是G的孩子。若是你是在考試時作這道題目,時間就是分數、名次、學歷,那麼你根本不需關心F是G的左仍是右孩子,前序遍歷序列的最終結果就是EACBDGF。
不過細細分析,根據中序序列ABCDEFG,是能夠得出F是G的左孩子。
從這裏咱們也獲得兩個二叉樹遍歷的性質。
但要注意了,已知前序和後序遍歷,是不能肯定一棵二叉樹的,緣由也很簡單,好比前序序列是ABC,後序序列是CBA,咱們能夠肯定A必定是根結點,但接下來,咱們沒法知道,哪一個結點是左子樹,哪一個是右子樹。這棵樹可能有如圖6-8-24所示的四種可能。
說了半天,咱們如何在內存中生成一棵二叉鏈表的二叉樹呢?樹都沒有,哪來遍歷。因此咱們還得來談談關於二叉樹創建的問題。
若是咱們要在內存中創建一個如圖6-9-1左圖這樣的樹,爲了能讓每一個結點確認是否有左右孩子,咱們對它進行了擴展,變成圖6-9-1右圖的樣子,也就是將二叉樹中每一個結點的空指針引出一個虛結點,其值爲一特定值,好比"#"。咱們稱這種處理後的二叉樹爲原二叉樹的擴展二叉樹。擴展二叉樹就能夠作到一個遍歷序列肯定一棵二叉樹了。好比圖6-9-1的前序遍歷序列就爲AB#D##C##。
有了這樣的準備,咱們就能夠來看看如何生成一棵二叉樹了。假設二叉樹的結點均爲一個字符,咱們把剛纔前序遍歷序列AB#D##C##用鍵盤挨個輸入。實現的算法以下:
其實創建二叉樹,也是利用了遞歸的原理。只不過在原來應該是打印結點的地方,改爲了生成結點,給結點賦值的操做而已。因此你們理解了前面的遍歷的話,對於這段代碼就不難理解了。
固然,你徹底也能夠用中序或後序遍歷的方式實現二叉樹的創建,只不過代碼裏生成結點和構造左右子樹的代碼順序交換一下。另外,輸入的字符也要作相應的更改。好比圖6-9-1的擴展二叉樹的中序遍歷字符串就應該爲#B#D#A#C#,然後序字符串應該爲###DB##CA。
咱們如今提倡節約型社會,一切都應該節約爲本。對待咱們的程序固然也不例外,能不浪費的時間或空間,都應該考慮節省。咱們再來觀察圖6-10-1,會發現指針域並非都充分的利用了,有許許多多的"^",也就是空指針域的存在,這實在不算好現象,應該要想辦法利用起來。
首先咱們要來看看這空指針又多少個呢?對於一個有n個結點的二叉鏈表,每一個結點有指向左右孩子的兩個指針域,因此一共有2n個指針域。而n個結點的二叉樹一共有n-1條分支線數,也就是說,實際上是存在2n-(n-1)=n+1個空指針域。好比圖6-10-1有10個結點,而帶有"^"空指針域爲11.這些空間不存儲任何事物,白白的浪費着內存的資源。
另外一方面,咱們在作遍歷時,好比對圖6-10-1作中序遍歷時,獲得了HDIBJEAFCG這樣的字符序列,遍歷事後,咱們能夠知道,結點I的前驅是D,後繼是B,結點F的前驅是A,後繼是C。也就是說,咱們能夠很清楚的知道任意一個結點,它的前驅和後繼是哪個。
但是這是創建在已經遍歷過的基礎之上的。在二叉鏈表上,咱們只能知道每一個結點指向其左右孩子結點的地址,而不知道某個結點的前驅是誰,後繼是誰。要想知道,必須遍歷一次。之後每次須要知道時,都必須先遍歷一次。爲何不考慮在建立時就記住這些前驅和後繼呢,那將是多大的時間上的節省。
綜合剛纔兩個角度的分析後,咱們能夠考慮利用那些空地址,存放指向結點在某種遍歷次序下的前驅和後繼結點的地址。就好像GPS導航儀同樣。咱們開車的時候,哪怕咱們對具體目的地的位置一無所知,但它每次均可以告訴我從當前位置的下一步應該走向哪裏。這就是咱們如今要研究的問題。咱們把這種指向前驅和後繼的指針稱爲線索,加上線索的二叉鏈表稱爲線索鏈表,相應的二叉樹就稱爲線索二叉樹(Threaded Binary Tree)。
請看圖6-10-2,咱們把這棵二叉樹進行中序遍歷後,將全部的空指針域中的rchild,改成指向它的後繼結點。因而咱們就能夠經過指針知道H的後繼是D(圖中1),I的後繼是B(圖中2),J的後繼是E(圖中3),E的後繼是A(圖中4),F的後繼是C(圖中5),G的後繼由於不存在而指向NULL(圖中6)。此時共有6個空指針域被利用。
再看圖6-10-3,咱們將這棵二叉樹的全部空指針域中的lchild,改成指向當前結點的前驅。所以H的前驅是NULL(圖中1),I的前驅是D(圖中2),J的前驅是B(圖中3),F的前驅是A(圖中4),G的前驅是C(圖中5)。一共5個空指針域被利用,正好和上面的後繼加起來是11個。
經過圖6-10-4(空心箭頭實線爲前驅,虛線黑箭頭爲後繼),就更容易看出,其實線索二叉樹,等因而把一棵二叉樹轉變成了一個雙向鏈表,這樣對咱們的插入刪除結點、查找某個結點都帶來了方便。因此咱們對二叉樹以某種次序遍歷使其變爲線索二叉樹的過程稱作是線索化。
不過好事老是多磨的,問題並無完全解決。咱們如何知道某一結點的lchild是指向它的左孩子仍是指向前驅?rchild是指向右孩子仍是指向前驅?好比E結點的lchild是指向它的左孩子J,而rchild倒是指向它的後繼A。顯然咱們在決定lchild是指向左孩子仍是前驅,rchild是指向右孩子仍是後繼上是須要一個區分標誌的。所以,咱們在每一個結點再增設兩個標誌域ltag和rtag,注意ltag和rtag只是存放0或1數字的布爾型變量,其佔用的內存空間要小於像lchild和rchild的指針變量,結點結構如表6-10-1所示。
其中:
由此二叉樹的線索存儲結構定義代碼以下:
線索化的實質就是將二叉鏈表中的空指針改成指向前驅或後繼的線索。因爲前驅和後繼的信息只有在遍歷該二叉樹時才能獲得,因此線索化的過程就是在遍歷的過程當中修改空指針的過程。
中序遍歷線索化的遞歸函數代碼以下:
你會發現,這代碼除加粗代碼之外,和二叉樹中序遍歷的遞歸代碼幾乎徹底同樣。只不過將本是打印結點的功能改爲了線索化的功能。
中間加粗部分代碼是作了這樣的一些事。
if(!p -> lchild)表示若是某結點的左指針域爲空,由於其前驅結點剛剛訪問過,賦值給了pre,因此能夠將pre賦值給p->lchild,並修改p->LTag=Thread(也就是定義爲1)以完成前驅結點的線索化。
後續就要稍稍麻煩一些。由於此時p結點的後繼尚未訪問到,所以只能對它的前驅結點pre的右指針rchild作判斷,if(!pre->rchild)表示若是爲空,則p就是pre的後繼,因而pre->rchild=p,而且設置pre->RTag=Thread,完成後繼結點的線索化。
完成前驅和後繼的判斷後,別忘了將當前的結點p賦值給pre,以便於下一次使用。
有了線索二叉樹後,咱們對它進行遍歷時發現,其實就等因而操做一個雙向鏈表結構。
和雙向鏈表結構同樣,在二叉樹線索鏈表上添加一個頭結點,如圖6-10-6所示,並令其lchild域的指針指向二叉樹的根結點(圖中的1),其rchild域的指針指向中序遍歷時訪問的最後一個結點(圖中的2)。反之,令二叉樹的中序序列中的第一個結點中,lchild域指針和最後一個結點的rchild域指針均指向頭結點(圖中的3和4)。這樣定義的好處就是咱們既能夠從第一個結點起順後繼進行遍歷,也能夠從最後一個結點起順前驅進行遍歷。
遍歷的代碼以下:
從這段代碼也能夠看出,它等因而一個鏈表的掃描,因此時間複雜度爲O(n)。
因爲它充分利用了空指針域的空間(這等於節省了空間),又保證了建立時的一次遍歷就能夠終生受用前驅後繼的信息(這意味着節省了時間)。因此在實踐問題中,若是所用的二叉樹需常常遍歷或查找結點時須要某種遍歷序列中的前驅和後繼,那麼採用線索二叉鏈表的存儲結構就是很是不錯的選擇。
我以前在網上看到這樣一個故事,不知道是真仍是假,反正是有點意思。
故事是說聯合利華引進了一條香皂包裝生產線,結果發現這條生產線有個缺陷:經常會有盒子裏沒裝入香皂。總不能把空盒子賣給顧客啊,他們只好請了一個學自動化的博士設計一個方案來分揀空的香皂盒。博士組織成立了一個十幾人的科研攻關小組,綜合採用了機械、微電子、自動化、X射線探測等技術,花了幾十萬,成功解決了問題。每當生產線上有空香皂盒經過,兩旁的探測器會檢測到,而且驅動一隻機械手把空香皂盒推走。
中國南方有個鄉鎮企業也買了一樣的生產線,老闆發現這個問題後大爲光火,找了個小工說:你把這個問題搞定,否則老子炒你魷魚。小工很快想出了辦法:他在生產線旁邊放了颱風扇猛吹,空皁盒天然會被吹走。
這個故事在網上引發了很大的爭議,我相信你們聽完後也會有很多的想法。不過我在這只是想說,有不少複雜的問題都是能夠有簡單辦法去處理的,在於你肯不願懂腦筋,在於你有沒有創新。
咱們前面已經講過了樹的定義和存儲結構,對於樹來講,在知足樹的條件下能夠是任意形狀,一個結點能夠有任意多個孩子,顯然對樹的處理要複雜得多,去研究關於樹的性質和算法,真的不容易。有沒有簡單的辦法解決對樹處理的難題呢?
咱們前面也講了二叉樹,儘管他也是樹,但因爲每一個結點最多隻能有左孩子和右孩子,面對的變化就少不少了。所以不少性質和算法都被研究了出來。若是全部的樹都像二叉樹同樣方便就行了。你還別說,真是能夠這樣作。
在講述的存儲結構時,咱們提到了樹的孩子兄弟法能夠將一棵樹用二叉鏈表進行存儲,因此藉助二叉鏈表,樹和二叉樹能夠相互進行轉換。從物理結構來看,他們的二叉鏈表也是相同的,只是解釋不太同樣而已。所以,只要咱們設定必定的規則,用二叉樹來表示樹,甚至表示森林都是能夠的,森林與二叉樹也能夠互相進行轉換。
咱們分別來看看他們之間的轉換如何進行。
將樹轉換爲二叉樹的步驟以下:
例如圖6-11-2,一棵樹通過三個步驟轉換爲一棵二叉樹。初學者容易犯的錯誤就是在層次調整時,弄錯了左右孩子的關係。好比圖中F、G本都是樹結點B的孩子,是結點E的兄弟,所以轉換後,F就是二叉樹結點E的右孩子,G是二叉樹結點F的右孩子。
森林是由若干棵樹組成的,因此徹底能夠理解爲,森林中的每一棵樹都是兄弟,能夠按照兄弟的處理辦法來操做。步驟以下:
例如圖6-11-3,將森林的三棵樹轉化爲一棵二叉樹。
二叉樹轉換爲樹是樹轉換爲二叉樹的逆過程,也就是反過來作而已。如圖6-11-4所示。步驟以下:
判斷一棵二叉樹可以轉換成一棵樹仍是森林,標準很簡單,那就是隻要看這棵二叉樹的根結點有沒有孩子,有就是森林,沒有就是一棵樹。那麼若是是轉換成森林,步驟以下:
最後咱們再談一談關於樹和森林的遍歷問題。
樹的遍歷分爲兩種方式。
森林的遍歷也分爲兩種方式:
可若是咱們對圖6-11-4的左側二叉樹進行分析就會發現,森林的前序遍歷和二叉樹的前序遍歷結果相同,森林的後序遍歷和二叉樹的中序遍歷結果相同。
這也就告訴咱們,當以二叉鏈表做樹的存儲結構時,樹的先根遍歷和後根遍歷徹底能夠藉助二叉樹的前序遍歷和中序遍歷的算法來實現。這其實也就證明,咱們找到了對樹和森林這種複雜問題的簡單解決辦法。
"喂,兄弟,最近無聊透頂了,有沒有什麼書可看?"
"我這有《三國演義》的電子書,你要不要?"
"'既生瑜,何生亮。'《三國演義》好呀,你郵件發給我!"
"OK!文件1M多大小,好像大了點。我打個包,稍等.....哈哈,少了一半,壓縮效果不錯呀。"
"太棒了,快點傳給我吧。"
這是咱們生活中常見的對白。如今咱們都是講究效率的社會,什麼都要求速度,在不能出錯的狀況下,作任何事情都講究越快越好。在計算機和互聯網技術中,文本壓縮就是一個很是重要的技術。玩電腦的人幾乎都會應用壓縮和解壓縮軟件來處理文檔。由於它除了能夠減小文檔在磁盤上的空間外,還有重要的一點,就是咱們能夠在網絡上以壓縮的形式傳輸大量數據,使得保存和傳遞都更加高效。
那麼壓縮而不出錯是如何作到的呢?簡單說,就是把咱們要壓縮的文本進行從新編碼,以減小沒必要要的空間。儘管如今最新技術在編碼上已經很好很強大,但這一切都來自於曾經的技術積累,咱們今天就來介紹一下最基本的壓縮編碼方法--赫夫曼編碼。
在介紹赫夫曼編碼前,咱們必須得介紹赫夫曼樹,而介紹赫夫曼樹,咱們不得不提這樣一我的,美國數學家赫夫曼(David Huffman),也有的翻譯爲哈夫曼。他在1952年發明了赫夫曼編碼,爲了記念他的成就,因而就把他在編碼中用到的特殊的二叉樹稱之爲赫夫曼樹,他的編碼方法稱爲赫夫曼編碼。也就是說,咱們如今介紹的知識全都來自於近60年前這位偉大科學家的研究成果,而咱們平時所用的壓縮和解壓縮技術也都是基於赫夫曼的研究之上發展而來,咱們應該要記住他。
什麼叫作赫夫曼樹呢?咱們先來看一個例子。
過去咱們小學、中學通常考試都是用百分制來表示學科成績的。這帶來了一個弊端,就是很容易讓學生、家長,甚至老師都以分取人,讓分數表明了一切。又是想一想也對,90分和95分也許就只是一道題目對錯的差距,但卻讓兩個孩子可能受到徹底不一樣的際遇,這並不公平。因而再現在提倡素質教育的背景下,咱們不少的學科,特別是小學的學科成績都該做了優秀、良好、中等、及格和不及格這樣模糊的詞語,再也不通報具體的分數。
不過對於老師來說,他在對試卷評分的時候,顯然不能憑感受給優良或及格不及格等成績,所以通常都仍是按照百分制算出每一個學生的成績後,再根據統一的標準換算得出五級分制的成績。好比下面的代碼就實現了這樣的轉換。
圖6-12-2粗略看沒什麼問題,但是一般都認爲,一張好的考卷應該是讓學生成績大部分處於中等或良好的範圍,優秀和不及格都應該較少纔對。而上面這樣的程序,就使得全部的成績都須要先判斷是否及格,再逐級而上獲得結果。輸入量很大的時候,其實算法是有效率問題的。
若是在實際的學習生活中,學生的成績在5個等級上的分佈規律如表6-12-1所示。
那麼70分以上大約佔總數80%的成績都須要通過3次以上的判斷才能夠得出結果,這顯然不合理。
有沒有好一些的辦法,仔細觀察發現,中等成績(70-79分之間)比例最高,其次是良好成績,不及格的所佔比例最少。咱們把圖6-12-2這棵二叉樹從新進行分配。改爲如圖6-12-3的作法試試看。
從圖中感受,應該效率要高一些了,到底高多少呢。這樣的二叉樹又是如何設計出來的呢?咱們來看看赫夫曼大叔是如何說的吧。
咱們先把這兩棵二叉樹簡化成葉子結點帶權的二叉樹,如圖6-12-4所示。其中A表示不及格、B表示及格、C表示中等、D表示良好、E表示優秀。每一個葉子的分支線上的數字就是剛纔咱們提到的五級分制的成績所佔比例數。
赫夫曼大叔說,從樹中一個結點到另外一個結點之間分分支構成兩個結點之間的路徑,路徑上的分支數目稱作路徑長度。圖6-12-4的二叉樹a中,根結點到結點D的路徑長度就爲4,二叉樹B中根結點到D的路徑長度爲2。樹的路徑長度就是從樹根到每一結點的路徑長度之和。二叉樹a的書路徑長度就爲1+1+2+2+3+3+4+4=20。二叉樹b的樹路徑長度就爲1+2+3+3+2+1+2+2=16。
若是考慮到帶權的結點,結點的帶權的路徑長度爲從該結點到樹根之間的路徑長度與結點上權的乘積。輸的帶權路徑長度爲樹中全部葉子結點的帶權路徑長度之和。假設有n個權值(w1,w2,...wn),構造一棵有n個葉子結點的二叉樹,每一個葉子結點帶權w(k),每一個葉子的路徑長度爲1k,咱們一般記做。則其中帶權路徑長度WPL最小的二叉樹稱作赫夫曼樹。也有很多書中也稱爲最優二叉樹,我我的以爲胃裏記念作出巨大貢獻的科學家,既然用他們的名字命名,就應該要堅持用他們的名字稱呼,哪怕"最優"更能體現這棵樹的品質也應該只做爲別名。
有了赫夫曼對帶權路徑長度的定義,咱們來計算一下圖6-12-4這兩棵樹的WPL值。
二叉樹a的WPL=5x1+15x2+40x3+30x4+10x4=315。
注意:這裏5是A結點的權,1是A結點的路徑長度,其餘同理。
二叉樹b的WPL=5x3+15x3+40x2+30x2+10x2=220。
這樣的結果意味着什麼呢?若是咱們如今有10000個學生的百分制成績須要計算五級分製成績,用二叉樹a的判斷方法,須要作31500次比較,而二叉樹b的判斷方法,只須要22000次比較,差很少少了三分之一量,在性能上提升不是一點點。
那麼如今的問題就是,圖6-12-4的二叉樹b這樣的樹是如何構造出來的,這樣的二叉樹是否是就是最優的赫夫曼樹呢?別急,赫夫曼大叔給了咱們解決的辦法。
此時的圖6-12-8二叉樹的帶權路徑長度WPL=40x1+30x2+15x3+10x4+5x4=205。與圖6-12-4的二叉樹b的WPL值220相比,還少了15。顯然此時構造出來的二叉樹纔是最優的赫夫曼樹。
不過現實老是比理想要複雜得多,圖6-12-8雖然是赫夫曼樹,但因爲每次判斷都要兩次比較(如根結點就是a<80 && a>=70,兩次比較才能獲得y或n的結果),因此在整體性能是哪一個,反而不如圖6-12-3的二叉樹性能高。固然這並非咱們要討論的重點了。
經過剛纔的步驟,咱們能夠得出構造赫夫曼樹的赫夫曼算法描述。
固然,赫夫曼研究這種最優樹的目的不是爲了咱們能夠轉化一下成績。他的更大目的是爲了解決當年遠距離通訊(主要是電報)的數據傳輸的最優化問題。
好比咱們有一段文字內容爲"BADCADFEED"要網絡傳輸給別人,顯然用二進制的數字(0和1)來表示是很天然的想法,咱們如今這段文字只有六個字母ABCDEF,那麼咱們能夠用相應的二進制數據表示,如表6-12-2所示。
這樣真正傳輸的數據就是編碼後的"001000011010000011101100100011",對方接收時能夠按照3位壹分來譯碼。若是一篇文章很長,這樣的二進制串也將很是的可怕。並且事實上,無論是英文、中文或是其餘語言,字母或漢字的出現頻率是不相同的,好比英語中的幾個元音字母"a e i o u",中文中的"的 了 有 在"等漢字都是頻率極高。
假設六個字母的頻率爲A 27,B 8,C 15,D 15,E 30,F 5,合起來正好是100%。那就意味着,咱們徹底能夠從新安置赫夫曼樹來規劃它們。
圖6-12-9左圖爲構造赫夫曼樹的過程的權值顯示。右圖爲將權值左分支改成0,右分支改成1後的赫夫曼樹。
此時,咱們對這六個字母用其從樹根到葉子所通過路徑的0或1來編碼,能夠獲得如表6-12-3所示這樣的定義。
咱們將文字內容爲"BADCADFEED"再次編碼,對比能夠看到結果串變小了。
也就是說,咱們的數據被壓縮了,節約了大約17%的存儲或傳輸成本。隨着字符的增長和多字符權重的不一樣,這種壓縮會更加顯出其優點。
當咱們接收到1001010010101001000111100這樣壓縮過的新編碼時,咱們應該如何把它解碼出來呢?
編碼中非0即1,長短不等的話實際上是很容易混淆的,因此若要設計長短不等的編碼,則必須是任一字符的編碼都不是另外一個字符的編碼的前綴,這種編碼稱作前綴編碼。
你仔細觀察就會發現,表6-12-3中的編碼就不存在容易與1001,1000混淆的"10"和"100"編碼。
可僅僅是這樣不足以讓咱們去方便地解碼的,所以在解碼時,仍是要用赫夫曼樹,即發送方和接收方必需要約定好一樣的赫夫曼編碼規則。
當咱們接收到1001010010101001000111100時,由約定好的赫夫曼樹可知,1001獲得第一個字母是B,接下來01意味着第二個字符是A,如圖6-12-10所示,其他的也相應的能夠獲得,從而成功解碼。
通常地,設須要編碼的字符集爲{d1,d2,..,dn},各個字符在電文中出現的次數或頻率集合爲{W1,W2,..,Wn},以d1,d2,...,dn做爲葉子結點,以W1,W2,..,Wn做爲相應葉子結點的權值來構造一棵赫夫曼樹。規定赫夫曼樹的左分支表明0,右分支表明1,則從根結點到葉子結點所通過的路徑分支組成的0和1的序列便爲該結點對於字符的編碼,這就是赫夫曼編碼。
終於到了總結的時間,這一章與前面章節相比,顯得過於龐大了些,緣由也就在於樹的複雜性和變化豐富度是前面的線性表所不可比擬的。即便在本章以後,咱們還要講解關於樹這一數據結構的相關知識,可見它的重要性。
開頭咱們提到了樹的定義,講到了遞歸在樹定義中的應用。提到了如子樹、結點、度、葉子、分支結點、雙親、孩子、層次、深度、森林等諸多概念,這些都是須要在理解的基礎上去記憶的。
咱們談到了樹的存儲結構時,講了雙親表示法、孩子表示法、孩子兄弟表示法等不一樣的存儲結構。
並由孩子兄弟表示法引出了咱們這章中最重要一種樹,二叉樹。
二叉樹每一個結點最多兩棵子樹,有左右之分。提到了斜樹,滿二叉樹、徹底二叉樹等特殊二叉樹的概念。
咱們接着談到它的各類性質,這些性質給咱們研究二叉樹帶來了方便。
二叉樹的存儲結構因爲其特殊性使得既能夠用順序存儲結構又能夠用鏈式存儲結構表示。
遍歷是二叉樹最重要的一門學問,前序,中序,後序以及層序遍歷都是須要熟練掌握的知識。要讓本身學會計算機的運行思惟去模擬遞歸的實現,能夠加深咱們對遞歸的理解。不過,並不是二叉樹遍歷就必定要用到遞歸,只不過遞歸的實現比較優雅而已。這點須要明確。
二叉樹的創建天然也是能夠經過遞歸來實現。
研究中也發現,二叉鏈表有不少浪費的空指針能夠利用,查找某個結點的前驅和後繼爲何非要每次遍歷才能夠獲得,這就引出瞭如何構造一棵線索二叉樹的問題。線索二叉樹給二叉樹的結點查找和遍歷帶來了高效率。
樹、森林看似複雜,其實它們均可以轉化爲簡單的二叉樹來處理,咱們提供了樹、森林與二叉樹的互相轉換的辦法,這樣就使得面對樹和森林的數據結構時,編碼實現成爲了可能。
最後,咱們提到了關於二叉樹的一個應用,赫夫曼樹和赫夫曼編碼,對於帶權路徑的二叉樹作了詳盡地講述,讓你初步理解數據壓縮的原理,並明白其是如何作到無損編碼和無錯解碼的。