數據結構與算法(八)—— 樹結構及其實現和應用

注:本篇內容參考了《Java常用算法手冊》、《大話數據結構》和《算法導論(第三版)》三本書籍。

本人水平有限,文中如有錯誤或其它不妥之處,歡迎大家指正!

 

目錄

1. 樹的概述

1.1 樹的定義

1.2 樹結構的特徵

1.3 樹的基本概念

1.3.1 度

1.3.2 樹的層次(Level)

1.3.3 樹的深度(De)

1.3.4 路徑

1.3.5 森林(forest)

1.3.6 結點

1.4 樹的分類

1.4.1 根據樹中子結點間的有沒有順序分類

1.5 樹的存儲方式

1.5.1 順序存儲

1.5.2 鏈表存儲

1.6 樹的存儲結構表示方法

1.6.1 雙親表示法

1.6.2 孩子表示法

1.6.3 孩子兄弟表示法

1.7 與線性表的區別

2. 二叉樹介紹

2.1 二叉樹定義

2.2 二叉樹的性質

2.3 二叉樹的分類

2.3.1 斜樹

2.3.2 滿二叉樹

2.3.3 完全二叉樹

2.3.4 二叉搜索樹

2.3.5 平衡二叉樹(AVL樹)

2.4 二叉樹的存儲結構

2.4.1 二叉樹的順序存儲

2.4.2 二叉樹的鏈式存儲

2.5 樹、森林與二叉樹的轉換

2.5.1 樹轉換爲二叉樹

2.5.2 森林轉換爲二叉樹

2.5.3 二叉樹轉換爲樹

2.5.4 二叉樹轉換爲森林

2.6 二叉樹的應用

3. 二叉樹的操作及Java代碼實現

3.1 二叉樹的初始化

3.2 添加結點

3.3 查找結點

3.4 獲取子樹

3.5 判斷空樹

3.6 計算二叉樹的深度

3.7 清空二叉樹

3.8 顯示結點數據

3.9 遍歷二叉樹

3.9.1 二叉樹的遍歷方法

3.9.2  前序遍歷方法

3.9.3 中序遍歷方法

3.9.4 後序遍歷方法

3.9.5 層序遍歷方法

3.9.6 推導遍歷結果

4.10 二叉樹的建立


 

1. 樹的概述

樹結構的數據在生活中應該見的比較多,像國家的行政機構,一個公司的組織機構等。它們有個共同點,就是都可以表示成一個層次關係,這種層次關係可以抽象爲樹結構。

 

1.1 樹的定義

樹(Tree)是 n (\bg_white \large n\geqslant 0)個結點的有限集。當n = 0時稱爲空樹。通俗一點講,樹是 n 個數據結構的集合,在該集合中包含一個根結點,根結點之下分佈着一些互不交叉的子集合,這些子集合也就是樹結點的子樹。就是說每一個子集合本身又是一棵樹,稱爲根(或原樹)的子樹(SubTree)。若樹結構中僅包含一個結點,那這也是一個樹,樹根便是該結點自身。樹的結點包含了一個數據元素及若干指向其子樹的分支。

從下圖可以看,樹結構跟植物的樹有所不一樣,樹結構是根在上,分支在下,是植物樹倒過來了。結點B、C及其下面的分支是根結點A的子樹。

由於樹結構不是線性結構,很難用數學表達式來表示,一般來說,常採用層次括號法來表示。層次括號的基本規則如下: 

  • 根結點放入一對圓括號中;
  • 根結點的子樹由左向右順序放入括號中;
  • 對子樹做上述相同的處理。

需要注意的是,樹根結點之下分佈着一些互不交叉的子集合。所以下圖中的樹是不符合樹的定義的,因爲它們交叉了。

1.2 樹結構的特徵

樹結構有以下幾個特徵:

  1. 樹結構是一種非線性結構;
  2. 在一個樹結構中,有且僅有一個結點沒有直接前驅,這個結點就是樹的根結點,稱爲根結點或根(Root)
  3. 除根結點外,其餘每個結點有且僅有一個直接前驅
  4. 每個結點可以有任意多個直接後繼。

在上圖的右圖中,可以看到越往下層根第分支越多。其中,A是樹的根結點,根結點A有兩個直接後繼結點B、C,而結點E只有一個直接前驅結點C。

 

1.3 樹的基本概念

如下圖,下面定義中的舉例使用此圖。 

1.3.1 度

這裏的度分爲樹的度和結點的度。

結點的度:是指一個結點所包含子樹的數量,在上圖中結點A有兩個子樹,因此結點A的度爲2,結點D的度爲3。

樹的度:也就是樹的寬度,是樹所有結點中最大的度。簡單來說就是樹結點的分支數。在上圖中,此樹中所有結點中最大度的爲3,所以此樹的度爲3。

1.3.2 樹的層次(Level)

簡單來說,就是樹的層級,從根結點開始算起。因爲根結點的層次爲1,所以樹的層次自然也是從1開始算起。根結點的層次爲1,依次向下爲2、3、...、n。樹是有種層次結構,每個結點都處在一定的層次上。樹中結點的最大層次稱爲樹的深度或高度。

1.3.3 樹的深度(De)

樹的深度是指樹中結點的最大層次,如上圖,此樹的深度爲4。有的地方也叫做高度。

1.3.4 路徑

對於一棵樹中的任意兩個不同的結點,若從一個結點出發,按層次自上而下沿着一個個樹枝能到達另一個結點,則稱這兩個結點之間一條路徑。可以用路徑所經過的結點序列表示路徑,路徑的長度等於路徑上的結點個數減1。

1.3.5 森林(forest)

森林(forest)是指 n (n > 0) 棵互不相交的樹的集合。

1.3.6 結點

父結點(Parent):每個結點的子樹的根稱爲該結點的子結點,相應地,該結點稱爲父結點。上圖中結點C是結點E和F的父結點 。

子結點(Child):每個結點的子樹的根稱爲該結點的子結點。上圖中結點E和F是結點C的子結點。

兄弟結點:具有同一個父結點的結點。上圖中,結點B和C就是兄弟結點,它們的父結點是結點A。

堂兄弟結點:父結點在同一層的結點互爲堂兄弟,上圖中的D、E、F結點就是堂兄弟結點。

結點的祖先:從根到該節點所經的分支上的所有節點。上圖中結點G的祖先A、B、D。

結點的子孫:以某結點爲根的子樹中任一結點都稱爲該節點的子孫。上圖中結點C的子孫E和J。

 葉結點(終端結點):樹中度爲零的結點稱爲葉結點或終端結點。上圖中,結點G、H、I都是葉結點,因爲它們沒有子樹,度爲0。

分支結點:樹中度不爲零的結點稱爲分對結點或非終端結點。上圖中結點B和C都是分支結點。

1.4 樹的分類

1.4.1 根據樹中子結點間的有沒有順序分類

根據樹中任意節點的子結點之間有沒有順序,可以分爲有序樹和無序樹。

有序樹是指若樹中各結點的子樹(兄弟結點)是按一定次序從左向右安排的,稱爲有序樹。即樹中任意結點的子結點之間有順序關係。有序樹是編程領域的基礎結構,大部分樹的變形都是基於有序樹演變而來。有序樹又可細分爲二叉樹、霍夫曼樹、B樹等

無序樹是指樹中任意節點的子節點之間沒有順序關係,也稱爲自由樹。無序樹在實際應用中意義不大。

 

1.5 樹的存儲方式

樹結構有兩種存儲結構,分別是順序存儲方式和鏈表存儲方式。

1.5.1 順序存儲

樹的順序存儲是用數組來存儲一棵二叉樹,具體存儲方法是將二叉樹中的結點進行編號,然後按編號依次將結點值存入到一個數組中,這樣就完成了一棵二叉樹的順序存儲。這種存儲結構比較適合存儲完全二叉樹,存儲一般的二叉樹會浪費大量的存儲空間,因爲完全二叉樹基本不會浪費數組空間。一般的二叉樹如果節點分佈不均勻,那就會出現大量空間被浪費。

順序存儲結構有一定的侷限性,它不便於存儲任意形態的二叉樹。

 

1.5.2 鏈表存儲

上面說了順序存儲結構不便於存儲任意形態的二叉樹。觀察二叉樹的形態,可以發現一個根節點與兩棵子樹有關係,因此設計一個含有一個數據域和兩個指針域的鏈式結點結構,data表示數據域,用於存儲對應的數據元素;lchild和rchild分別表示左指針域與右指針域,它們分別用於存儲左子結點和右子結點的位置。若沒有右子結點,則右指針爲空。

1.6 樹的存儲結構表示方法

上面說明樹的兩種存儲方式,順序存儲結構是用一段連續的存儲單元依次存儲線性表的數據元素。這在存儲線性表時是很自然的,但對於樹這樣一多對的結構呢?

樹結構中某個結點的子結點可以有多個,這就意味着無論按何種順序將樹中所有結點存儲到數組中,結點的存儲位置都無法直接反映邏輯關係。因爲不知道該結點的子結點和父結點信息。可以說簡單的順序存儲結構不能滿足樹結構的實現要求。

但充分利用順序存儲和鏈式存儲結點的特點,完全可以實現對樹的存儲結點的表示。下面介紹三種表示法:雙親表示法、孩子表示法和孩子兄弟表示法。

1.6.1 雙親表示法

假設以一組連續的空間來存儲樹的結點,同時在每個結點中,附設一個指針來指示其雙親結點到鏈表中的位置。每個結點除了自己知道自己是誰以外,還知道它的雙親在哪。如下圖所示。

其中,data是數據域,用於存儲結點的數據信息。而parent是指針域,用於存儲該結點的雙親在數組中的下標。這樣以來,可以根據結點的parent指針很容易找到它的雙親結點,時間複雜度爲O(1)。但若要知道該結點的孩子時,需要遍歷整個結構纔行。如下圖所示(parent爲-1時表示該結點爲根結點)。

要知道某個結點的孩子信息時很麻煩,所以需要對此結構進行改進,改進方法就是再增加一個指針域表示該結點的最左邊孩子的域,不妨叫它長子域。這樣就可以很容易得到結點的孩子信息。如下圖所示(parent爲-1時表示該結點爲根結點,firstchild爲-1時表示該結點沒有子結點)。

對於有0或1個子結點來說,上來的改進之後已經可以解決查找子結點的問題了。那如果有多個孩子?或者說很關注兄弟結點之間的關係,需要知道兄弟結點的關係呢?也可以增加一個右兄弟域來體現。但子結點一多,只要超過2個,這樣增加一個域的表示方法就很麻煩了。

 

1.6.2 孩子表示法

現在換一種完全不同的考慮方法。由於樹中每個結點可能有很多棵子樹,可以考慮用多重鏈表,即每個結點有多個指針域,其中每個指針指向一棵子樹的根結點,把這種方法叫做多重鏈表示法。不過樹的每個結點的度,也就是它的孩子的個數是不同的,所以可以設計兩種方案來解決。

方案一

指針域的個數就等於樹的度。如下圖所示。其中data是數據域,child1到childd是指針域,用於指向該結點的子結點。

 以上圖的樹結構的圖來說,樹的度是3,所以打針域的個數是3,實現如下圖所示。可以發現這種方法對於樹中各結點的度相差很大的情況,很浪費存儲空間,很多指針域都是空的。但當樹的各結點的度相差很小時,開闢的空間基本是被充分利用了。

方案二

對於方案一可能存儲空間浪費的情況,有了方案二。這裏是按需分配空間的。每個結點指針域的個數等於該結點的度,專門取一個位置來存儲指針域的個數,其結構如下圖。

其中data是數據域,degree爲度的域,存儲該結點的子結子的個數,child1到childd是指針域,用於指向該結點的子結點。

這種方案克服中浪費存儲空間的缺點,提高了存儲空間的利用率。但由於各結點的鏈表是不相同的結構,加上要維護結點的度的值,在運算上會帶來時間的損耗。

從上面可以看出,把每個結點放到了一個順序存儲結構的數組中是合理的,效率很高,但每個結點的子結點的數量不確定的,所以再對每個結點的子結點建立一個單鏈表來體現這些子結點的關係。這就是孩子表示法,具體方法是把每個結點的子結點排列起來,然後以單鏈表作爲存儲結點,則 n 個結點有 n 個子鏈表,若是葉結點則此單鏈表爲空。最後 n 個頭指針又組成一個線性表,採用順序存儲結構,存放進一個一維數組中。如下圖所示。

爲此,設計兩種結點結構,一個是子鏈表的子結點,如下圖所示。其中child是數據域,用於存儲某個結點在表頭數組中的下標,next是指針域,用於存儲指向某結點的下一個子結點的指針。

另一個是表頭數組的表頭結點,如下圖所示。其中data是數據域,存儲某個結點的數據信息,firstchild是頭指針域,用於存儲該結點的子鏈表的頭指針。

這樣的結構對於要查找某個結點的子結點,或者查找某個結點的兄弟結點,只需要查找該結點的子鏈表即可。對於遍歷整棵樹也是很方便的,對頭結點的數組循環即可。但這也存在着問題,那就是如何知道某個結點的雙親是誰呢?比較麻煩,需要遍歷整棵樹纔行。把雙親表示法和孩子表示法綜合一下就好了。如下圖所示。把這種方法稱爲雙親孩子表示法,可以說是對孩子表示法的改進。

1.6.3 孩子兄弟表示法

上面從雙親和孩子的角度去研究了樹的存儲結構,如果從樹結點的兄弟角度去看,會是怎麼樣的呢?當然從整棵樹的來說,只研究結點的兄弟是不行的。通過觀察發現,任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此設置兩個指針,分別指向該結點的第一個子結點和此結點的右兄弟結點。結構如下圖。

其中,data是數據域,firstchild爲指針域,用於存儲該結點的第一個子結點的存儲地址,rightsib是指針域,用於存儲該結點的右兄弟結點的存儲地址。

這種表示法,爲查找某個結點的某個子結點帶來了方便,只需要通過firstchild找到此結點的長子結點,然後再通過長子結點的rightsib找到它的二弟,接着一直這樣下去,直到找到具體的孩子。但若想找到某個結點的雙親結點,這個表示法也是有缺陷的,所以可以增加一個parent指針域來解決快速雙親結點的問題。

此表示法的最大好處是它把一棵複雜的樹變成了一棵二叉樹。如下圖。這樣就可以充分利用二叉樹的特性和算法來處理這棵樹了。

 

1.7 與線性表的區別

線性表的第一個元素沒有直接前驅,最後一個數據元素沒有直接後繼,中間的元素只有一個直接前驅和直接後繼。樹結點的根結點是唯一的,葉結點可以有多個,葉結點是沒有子結點,中間的結點有一個父結點且有多個子結點。

 

2. 二叉樹介紹

這一節主要介紹二叉樹的定義、二叉樹的分類及二叉樹的存儲方式等內容。

2.1 二叉樹定義

二叉樹(Binary  Tree)是樹 n ( n >= 0)個結點的有限集合,每個結點最多含有兩個子樹的樹,或者說每個結點最多隻能兩個子結點。二叉樹的一個結點上對應的兩個子樹分別稱爲左子樹和右子樹。一個二叉樹結構也可以是空樹,空的二叉樹中沒有數據結點,也就是一個空集合。下圖中的左圖就是二叉樹,而左圖就不是二叉樹,因爲結點D有三個子樹。

根據二叉樹子樹的位置的個數,二叉樹一共有五種形式,除了空二叉樹和只有一個根結點的二叉樹外,還有如下圖中的三種形式。對於圖(a),只有一個子結點且位於左子樹位置,右子樹爲空。對於圖(b),只有一個子結點且位於右子樹位置,左子樹爲空。對於圖(c),左子樹和右子樹都存在,具有完整的兩個子結點。

二叉樹有如下特點

  1. 每個結點最多有兩棵樹,所以二叉樹中不存在度大於2的結點。要注意這裏是最多隻有棵子樹,可以沒有或有一棵或有兩棵子樹;
  2. 左子樹和右子樹是有順序的,次序不能任意顛倒。好比人的雙手,左手和右手顯然是不一樣的;
  3. 即使樹中某個結點只有一棵子樹,也要區分它是左子樹還是右子樹

在普通的樹結構中,結點的最大度數沒有限制,而二叉樹結點的最大度數爲2。另外,樹結構中沒有左子樹和右子樹的區分,而二叉樹中則有這個區別

二叉樹是樹結構中最簡單的一種形式,在研究樹結構時,二叉樹一般會是重點。因爲二叉樹的描述相對簡單,處理也相對簡單,而且更爲重要的是任意的樹都可以轉換成對應的二叉樹。因此二叉樹是所有樹結構的基礎。

 

2.2 二叉樹的性質

二叉樹有如下的性質,理解下面的性質,將有助於更好的使用它。

A,二叉樹的第 i 層最多擁有2^{i-1}個結點(i >= 1)

比如第二層的結點數最多有2^{2-1} = 2個;第三層的結點數最多有2^{3-1}=4個。

B,深度爲 k 的二叉樹最多共有2^{k}-1個結點(k >= 1)

比如樹有一層,最多有2^{1}-1 = 1 個結點;樹有二層,最多有2^{2}-1 = 3 個結點。

C, 對任何一棵非空的二叉樹T,若其葉結點數爲n_{0},分支度爲2的結點數爲n_{2},則n_{0}=n_{2} + 1。

葉結點數其實就是終端結點數。一棵二叉樹,除了葉結點外,剩下的就是度爲1或2的結點數了,設n_{1}爲度1的結點數,則樹T的結點總數n = n_{0} + n_{1} + n_{2}

如下圖中,結點總數是10,其中,A、B、C、D結點的度爲2,即n_{2} = 4,E結點的度爲1,即n_{1} = 1,F、G、H、I、J結點的度爲0,即n_{0} = 5。

換一個角度,來數一數它的連接線,由於根結點只有分支出去,沒有分支進入,所以分支線總數爲結點總數減去1,就是9個分支。A、B、C、D結點都有兩個分支線出去,E結點有一個分支線出去。用代數表達就是分支線總數= n - 1 = n_{1} + 2n_{2}。剛纔有等式 n =  n_{0} + n_{1} + n_{2},所以 n_{0} + n_{1} + n_{2}  - 1 =  n_{1} + 2n_{2}。得到 n_{0} = n_{2} + 1。

 

2.3 二叉樹的分類

2.3.1 斜樹

斜樹一定是斜的,所有的結點都只有左子樹的二叉樹叫左斜樹;所有結點都是隻有右子二叉樹叫右斜樹,兩者統稱爲斜樹。斜樹跟線性表很像,所以從這點來說,線性表可以理解爲樹的一種特殊形式。

 

2.3.2 滿二叉樹

在一棵樹中,若所有分支結點都存在左子樹和右子樹,並且所有葉結點都在同一層上,這樣的二叉樹稱爲滿二叉樹。滿二叉樹是所有葉結點都在最底層的完全二叉樹。

單是每個結點都存在左子樹和右子樹,並不能算滿二叉樹。還必須要所有的葉結點都在同一層上,這樣就使整棵樹達到了平衡。因此滿二叉樹有以下特點:

  1. 葉結點只能出現在最底層;
  2. 非葉結點的度一定是2,即都存在左子樹和右子樹;
  3. 在同樣深度的二叉樹中,滿二叉樹的結點個數最多,葉結點數也最多。

 

2.3.3 完全二叉樹

對一棵具有 n 個結點的二叉樹,按層序編號,若編號爲 i (1 \leqslant i \leqslant n)的結點與同樣深度的滿二叉樹中編號爲 i 的結點在二叉樹中位置完全相同,則這棵樹稱爲完全二叉樹對於一棵二叉樹,假設其深度爲 d (d > 1)。除了第 d 層外,其它各層的結點數目均已達到最大值,且第 d 層所有節點從左向右連續地緊密排列。其中完全二叉樹又包括了滿二叉樹。簡單來說,完全二叉樹就是少了側的若干結點。

 滿二叉樹和完全二叉樹是有區別的。首先滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿的。

其次,完全二叉樹的所有結點與同樣深度的滿二叉樹,在按層序編號相同的結點,它們是一一對應的。下圖中的樹1,因爲結點爲5的沒有左子樹卻有右子樹,那就使得按層序編號的第10個編號空了。同樣的道理,下圖中的樹2,由於結點3沒有子樹,使得6和7兩個編號的位置也空了。下圖中的樹3又是因爲編號爲5的結點沒有子造成第10和11的位置空了。所以它們都不是完全二叉樹,更不是滿二叉樹。

根據上面的內容,也可以得出一些關於完全二叉樹的特點:

  1. 葉結點只能出現在最下兩層
  2. 最下層的葉結點一定集中在左部連接位置
  3. 倒數第二層,若有葉結點,一定都在右部連續位置
  4. 若結點度爲1,則該結點只有位於左邊的子結點,即不存在只在位於右邊子結點的情況;
  5. 同樣結點數的二叉樹,完全二叉樹的深度最小。 

從上面的例子中,也可以瞭解到一個判斷二叉樹是否是完全二叉樹的辦法那就是對樹的圖示按照滿二叉樹的結構逐層編號,若序編號出現空檔(即中間斷了),就說明不是完全二叉樹,否則就是。

 

完全二叉樹的性質

(1)具有 n 個結點的完全二叉樹的深度爲log_{2}n+1log_{2}n的結果,是去掉小數後的最大整數)。

對於這條性質,下面來進行推導。深度爲k 的滿二叉樹的結點數 n 一定是2^{k}-1。這個值是最多的結點個數。根據結點個數倒推滿二叉樹的度數爲 k = log_{2}(n+1),比如結點數爲15的滿二叉樹,度爲4。

完全二叉樹的結點數一定是小於或等於同樣度數的滿二叉樹的結點個數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 <= log_{2}n < k,而 k 作爲度數也只能是整數,因此有 k = log_{2}n+1,其中log_{2}n是去掉小數後的整數。

(2)若對一棵有 n 個結點的完全二叉樹(其深度爲log_{2}n+1,去掉小數)的結點按層序編號(從第1層到深度值的一層,每層從向左往右),對任意一個結點 i (1 <= i <= n)有:

  1. 若 i = 1,則結點 i 是二叉樹的根結點,無雙親結點;若 i > 1,則結點 i 雙親結點是結點 i / 2(去掉小數後的值);
  2. 若 2i <= n,則結點 i 左邊的子結點是結點2i;若 2 * i > n,則結點 i 無左邊的子結點,沒有左子樹進一步也就沒有右子樹(即結點 i 爲葉結點或終端結點);
  3. 若2i+1 > n,則結點 i 無右邊的子結點;若 2*i + 1 <= n,則結點 i 右邊的子結點(即結點 i 的右子樹的根結點)是結點2i + 1。

在上圖爲例,來理解這個性質。上圖是一個完全二叉樹,度爲4,結點總數10。對第1條,i = 1時結點1就是根結點;i = 5時時雙親結點是5/2 = 2.5,取整數爲2,沒毛病。

對第二條,n = 10,取 i = 6,2*6 = 12 > 10,結點6沒有子結點,即它是葉結點;取 i = 4,2*4 = 8 < 10,結點4的左邊的子結點是2*4 = 8。

對第三條,取 i = 5, 2*5+1 = 11 > 10,結點5只有左邊的子結點10,沒有右邊的子結點;取 i = 6,2*6 +1 = 13 > 10,結點6

也沒有右邊的子結點。

 

完全二叉樹的應用

完全二叉樹可應用在堆結構中,進行堆排序等。

 

2.3.4 二叉搜索樹

二叉搜索樹,Binary  Search  Tree,也稱二叉查找樹、有序二叉樹、二叉排序樹。使用一棵二叉搜索樹既可以作爲一個字典又可以作爲一個優先隊列。 它有以下特徵:

  1. 若左子樹不爲空,則左子樹的所有結點的值都小於它的根結點的值;
  2. 若右子樹不爲空,則右子樹的所有節點的值都大於根節點的值;
  3. 左右子樹也分別爲二叉搜索樹;
  4. 沒有鍵值相等的節點。

 

2.3.5 平衡二叉樹(AVL樹)

含有相同節點的二叉搜索樹可以有不同的形態,而二叉搜索樹的平均查找長度與樹的深度有關,所以需要找出一個查找平均長度最小的一棵樹,那就是平衡二叉樹。它是基於二分法的策略提高數據的查找速度的二叉樹。平衡二叉樹,也叫AVL樹,它是一種結構平衡的二叉搜索樹,它有以下幾點特徵:

  1. 葉結點高度差的絕對值不超過1;
  2. 左右兩個子樹都是一棵平衡二叉樹;
  3. 二叉樹節點的平衡因子定義爲該結點的左子樹的深度減去右子樹的深度,則平衡二叉樹的所有結點的平衡因子只可能是-1,0,1。

 

紅黑樹是一種自平衡二叉查找樹,它於1972年由魯道夫 \cdot 貝爾發明,他稱之爲“對稱二叉B樹”。它在平衡二叉樹的基礎上每個結點又增加了一個顏色的屬性,節點的顏色只能是紅色或者黑色。它有以下幾點特徵:

  1. 根結點只能是黑色;
  2. 紅黑樹中所有的葉結點後面再接左右兩個空結點,這樣可以保證算法的一致性,且所有的空結點都是黑色;
  3. 其它的結點的顏色只能是紅色或黑色,紅色結點的父結點和左右子結點都是黑色,及黑色紅紅相同;
  4. 在任何一棵子樹中,從根結點向下到達空結點的路徑上所經過的黑節點的數目相同,從而保證了是一個平衡二叉樹。

 

2.4 二叉樹的存儲結構

前面講述過了樹的存儲,順序結構對存儲樹結構這種一對多關係的結構,實現再起比較困難。而二叉樹是一種特殊的樹結構,也是樹結構的基礎,鑑於二叉樹的特殊性和重要性,這裏再研究下它的存儲結構。

2.4.1 二叉樹的順序存儲

順序存儲方式是最基本的數據存儲方式。與線性表類似,二叉樹的順序存儲一般也是採用一維數組來表示。關鍵是要定義合適的次序來存儲樹中各個層次的數據。

下圖中二叉樹結點的數據類型爲字符。採用順序存儲時可以按層來存儲。即先存儲根結點,再從左向右依次存儲下一個結點的數據,直到所有的結點數據完全存儲。

將上圖中的二叉樹存入數組中,相應的下標對應結點的位置。如下圖所示。

由於二叉樹定義的嚴格,所以用順序結構可以表現出二叉樹的結構。對一般的二叉樹,儘管層序編號不能反映邏輯關係,但可以將其按完全二叉樹編號,只不過把不存在的結點設置爲空即可。如下圖所示,淺色結點表示不存在此結點。

 

 

考慮到一種極端情況,一棵深度爲 k 的右斜樹,它只有 k 個結點,卻需要分配2^{k}-1個存儲單元空間,這顯然是對存儲空間的浪費。如下圖所示。所以順序存儲結構一般只用於完全二叉樹對於更一般的情況,建議採用鏈式存儲方式

2.4.2 二叉樹的鏈式存儲

即然順序存儲方式適用性不夠強,就考慮鏈式存儲方式。二叉樹每個結點最多有兩個子結點,所以爲它設計一個數據域和兩個指針域,稱這樣的鏈表叫做二叉鏈表

如上圖,其中data用於存儲當前結點的數據,lchild和rchild都是指針域,分別用於存儲左邊子結點和右邊子結點的指針。

有時爲了方便,也可以保存該結點的父結點的引用。這樣就多了一個指針域,指向父結點的地址。稱之爲三叉鏈表

 

2.5 樹、森林與二叉樹的轉換

對於樹來說,滿足樹的條件下可以是任意形狀,一個結點可以有任意多個子結結點,顯然對樹的處理要複雜的多,去研究關於樹的性質和算法,真的不容易。所以需要找到簡單的辦法來解決對樹處理的問題。

上面介紹了二叉樹,二叉樹的每個結點最多隻能有一個左子樹和右子樹,這樣以來,相比普通樹二叉樹的變化就少很多了。如果所有樹都能像二叉樹一樣方便簡單就好處理了。所以處理上面問題的一個思路就是將普通樹轉換爲二叉樹後再處理。

在描述樹的存儲結構時,提到了樹的孩子兄弟法可以將一棵樹利用二叉鏈表進行存儲,所以藉助二叉鏈表,樹和二叉樹可以實現相互的轉換。從物理結構來看,它們的二叉鏈是相同的,只是解釋會不太一樣。因此只要設置一定的規則,用二叉樹來表示樹,甚至表示森林都是可以的,這樣森林也可以與二叉樹進行相互轉換。

 

2.5.1 樹轉換爲二叉樹

將樹轉換爲二叉樹的一般步驟如下:

  1. 加線:在所有兄弟結點之間加一連接線;
  2. 去線:對樹中的每個結點,只保留它與第一個子結點的連接線,刪除它與其它子結點間的連接線;
  3. 層次調整:以樹的根結點爲軸心,將整棵樹順時針旋轉一定的角度,使之結構層次分明。這裏需要注意的是,第一個子結點是二叉樹某個結點的左子結點,兄弟結點轉換過來的子結點是某個結點的右子結點。

如下圖所示。容易犯的一個錯誤就是在層次調整時,弄錯左右子結點的關係。 

2.5.2 森林轉換爲二叉樹

森林是由若干棵樹組成的,可理解爲:森森中的每棵樹都是兄弟關係,所以可以按照兄弟結點的處理辦法來操作。步驟如下:

  1. 把每棵樹轉換成二叉樹;
  2. 第一棵二叉樹不動,從第二棵二叉樹開始,依次把後一棵二叉樹的根結點作爲前一棵二叉樹的根結點的右子結點,並用連接線連起來。當所有的二叉樹連接起來後就得到了由森林轉換而來的二叉樹。

如下圖所示。將森林的三棵樹轉換爲一棵二叉樹。

2.5.3 二叉樹轉換爲樹

二叉樹轉換爲樹是樹轉換爲二叉樹的逆向過程,反過來操作。步驟和示圖如下:

  1. 加線:若某個結點的左子結點存在,則將這個左子結點的右子結點、右子結點的右子結點、右子結點的右子結點的右子結點...,就是左子結點的 n 個右子結點都作爲此結點的子結點。將該結點與這些右子結點用連接線連接起來。
  2. 去線:刪除原二叉樹中所有結點與其右子結點的連接線;
  3. 層次調整:使之結點層次分明。

2.5.4 二叉樹轉換爲森林

判斷一棵二叉樹能夠轉換成一棵樹還是森林,只要看這棵二叉樹的根結點有沒有右子結點即可:有則可以轉換成森林,沒有則可以轉換成一棵樹。步驟如下:

  1. 從根結點開始,若右子結點存在,則把與右子結點的連接線刪除,再查看分離後的二叉樹,若右子結點存在,則將連接線刪除...,直到所有右子結點的連接線都刪除爲止,然後得到分離後的二叉樹;
  2. 再將每棵分離後的二叉樹轉換爲樹即可。

2.6 二叉樹的應用

二叉樹的一個性質是一棵平均二叉樹的深度要比其結點個數小的多,這個性質很重要。尤其對於特殊類型的二叉樹即二叉搜索樹而言,其深度的平均值是O(logn),這將大大降低查找的時間複雜度。在普通樹的基礎上又發展了更爲有利於實際應用的特殊二叉樹,比如後面將要介紹的平衡樹、紅黑樹等。

但二叉樹在運用的不好的情況下,將會產生嚴重的問題。比如樹的深度擴大到了N-1(N是樹的結點個數),這樣的情況是不允許的。這種樹也被稱爲不平衡樹。

 

3. 二叉樹的操作及Java代碼實現

二叉樹的操作,包含了初始化、添加結點、查找結點、獲取左子樹、獲取右子樹、計算二叉樹深度、遍歷二叉樹等操作。在實際用到二叉樹時,這些操作基本都會涉及或使用到。下面是二叉樹結點類的代碼,在二叉樹操作的代碼示例中會用到。

public class LinkedTree<T> {

    private static final int MAXLENGTH = 64;

    /**
     * 根結點,根結點的父結點爲空
     */
    private TreeNode<T> root;    


   /**
     * 結點個數
     * @return
     */
    public int size() {
        return getSize(root);
    }


    private int getSize (TreeNode<T> tree) {
        if (null == tree) {
            return 0;
        } else {
            return 1 + getSize(tree.left) + getSize(tree.right);
        }
    }


   /**
     * 獲取根結點
     * @return
     */
    public TreeNode<T> getRoot() {
        return root;
    }


    public static void main(String[] args) {
        TreeNode<String> root = new TreeNode<String>("A");
        TreeNode<String> left = new TreeNode<String>("B");
        TreeNode<String> right = new TreeNode<String>("C");

        TreeNode<String> left1l = new TreeNode<String>("D");
        TreeNode<String> left1r = new TreeNode<String>("E");

        TreeNode<String> left2l = new TreeNode<String>("H");
        TreeNode<String> left3r = new TreeNode<String>("K");

        left2l.right = left3r;

        left1l.left = left2l;

        left.left = left1l;
        left.right = left1r;

        TreeNode<String> right1l = new TreeNode<String>("F");
        TreeNode<String> right1r = new TreeNode<String>("G");

        right.left = right1l;
        right.right = right1r;

        root.left = left;
        root.right = right;

        LinkedTree<String> tree = new LinkedTree<>();
        TreeNode<String> node = root;

//        tree.traversalPreOrderScan(node);
//        tree.traversalPreOrder(node);
//        tree.traversalInOrder(node);
//        tree.traversalPostorder(node);
        tree.traversalByLevel(node);
    }


       
    /**
     * 樹結點類
     */
    static class TreeNode<T>{

        /**
         * 當前結點元素數據
         */
        private T data;

        /**
         * 左子結點
         */
        private TreeNode<T> left;

        /**
         * 右子結點
         */
        private TreeNode<T> right;

        /**
         * 父結點
         */
//        private TreeNode<T> parent;


        public TreeNode() { }

        public TreeNode(T data) {
            super();
            this.data = data;
        }


        public TreeNode(T data, TreeNode<T> left, TreeNode<T> right) {
            this.data = data;
            this.left = left;
            this.right = right;
        }
    }
}

3.1 二叉樹的初始化

在使用順序存儲方式存儲二叉樹時,首先要初始化。設置樹的根結點爲空,其它的一些初始化工作根據具體需要來進行。在代碼實現時,可以使用構造函數來實現,也可以使用專門的初始化方法來完成初始化。

 /**
     * 構造函數,初始化
     */
    public LinkedTree() {
        root = null;
    }

3.2 添加結點

添加一個結點到二叉樹中,添加時,要指定其父結點,以及添加的結點是左子樹還是右子樹。

/**
     * 添加結點
     * @param node
     */
    public void add(TreeNode<T> node) {
        if ((node = new TreeNode<T>()) != null) {
            throw new RuntimeException("node is null when add");
        }
        TreeNode<T> pnode = null, parent = null;
        T data = null;

        /**
         * 設置左右子樹爲空
         */
        pnode.data = node.data;
        pnode.left = pnode.right = null;

        /**
         * 查找父結點
         */
        parent = findNode(node, data);
        if (null == parent) {
            pnode = null;
            throw new RuntimeException("can not find parent node, please set parent node");
        }
        parent.left = node.left;
        parent.right = node.right;
    }

3.3 查找結點

查找結點就是遍歷二叉樹中的每一個結點,逐個比較數據,當找到目標數據時將返回該數據所在結點的引用。

/**
     * 查找指定數據所在的結點
     *
     * @param node
     * @param data
     * @return
     */
    public TreeNode<T> findNode (TreeNode<T> node, T data) {
        TreeNode<T> tn;

        if (null == node) {
            return null;
        } else {
            if (node.data.equals(data)) {
                return node;
            } else if ((tn = findNode(node.left, data)) != null){
                return tn;
            } else if ((tn = findNode(node.right, data)) != null) {
                return tn;
            } else {
                return null;
            }
        }
    }

3.4 獲取子樹

獲取左子樹:就是返回當前結點的左子樹結點的數據。

獲取右子樹:就是返回當前結點的右子樹結點的數據。

/**
     * 獲取左子樹
     * @param node
     * @return
     */
    public TreeNode<T> getLeftNode (TreeNode<T> node) {
        return null != node ? node.left : null;
    }


    /**
     * 獲取右子樹
     * @param node
     * @return
     */
    public TreeNode<T> getRightNode (TreeNode<T> node) {
        return null != node ? node.right : null;
    }

3.5 判斷空樹

判斷空樹是判斷一個二叉樹結構是否爲空。若是空樹,則表示該二叉樹結構中沒有數據。

   /**
     * 是否空樹,判斷二叉樹是否爲空,若空則沒有數據
     * @return
     */
    public boolean isEmpty() {
        return root == null;
    }

   /**
     * 是否爲空
     * @param tree
     * @return
     */
    public boolean isEmpty(TreeNode<T> tree) {
        return null == tree ? true : false;
    }

3.6 計算二叉樹的深度

計算二叉樹的深度就是計算二叉樹中結點的最大層數。這裏往往需要採用遞歸算法來實現,需要計算左右子樹的深度來進行比較。

 /**
     * 深度
     * @return
     */
    public int depth() {
        return getHeight(root);
    }

    private int getHeight(TreeNode<T> tree) {
        if (null == tree) {
            return 0;
        } else {
            int leftHeight = getHeight(tree.left);
            int rightHeight =  getHeight(tree.right);
            return leftHeight > rightHeight ? 1 + leftHeight : 1 + rightHeight;
        }
    }

3.7 清空二叉樹

清空二叉樹就是將二叉樹變成一個空樹。可能需要使用遞歸算法來實現,它需要清空該結點的數據,也要清空該結點的左右子樹內容。

/**
     * 清空,將二叉樹變成一個空樹,遞歸清空
     * @return
     */
    public void clean(TreeNode<T> tree) {
        if (null != tree) {
            clean(tree.left);
            clean(tree.right);
            tree = null;
        }
    }

3.8 顯示結點數據

顯示結點的數據,這裏單純的在控制檯打印一下。實際怎麼操作根據需要處理。

/**
     * 顯示結點數據
     * @param node
     */
    public void data(TreeNode<T> node) {
        System.out.printf("%s", null != node.data ? node.data.toString() : "");
    }

 

3.9 遍歷二叉樹

遍歷二叉樹就是逐個查找整個二叉樹中所有的結點,它是二叉樹的基本操作,很多操作都需要首先遍歷整個二叉樹。下面舉例來說明二叉樹的遍歷。

假設手裏有20張100元和2000張1元的獎勵券,同時灑向空中,看誰最終搶的最多。對這個問題,很多人都會先去搶100元的。因爲100元比1元面值大很多,可以抵100張1元的券,這樣效果好的就不止一點點。同樣是搶券,在有限的時間內,要達到最高的效率,次序很重要。對二叉樹的遍歷來說,次序同樣很重要。

二叉樹的遍歷(traversing  binary  tree)是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點被訪問一次且僅被訪問一次。請注意這裏出現的兩個關鍵詞:訪問和次序。

訪問的目的是根據需求來的,可以是計算或打印等,它算是一個抽象操作。這裏可以假定是簡單的輸出結點的數據信息。

二叉樹的遍歷次序不同於線性結構,最多也就是從頭到尾、循環、雙向等簡單的遍歷方式。樹的結點之間不存在唯一的前驅和後繼關係,在訪問一個結點後,下一個被訪問的結點面臨着不同的選擇。選擇的不同,遍歷的次序也就不同。

 

3.9.1 二叉樹的遍歷方法

下圖表示二叉樹中的基本結構,D表示根結點,L表示左子樹,R表示右子樹。二叉樹的遍歷方式有很多種,如果用L、D、R分別表示遍歷左子樹、訪問根結點、遍歷右子樹,則二叉樹的遍歷方法可以有6種(3的排列組合,3!=6),分別是LDR、LRD、DLR、DRL、RDL、RLD。若限制了從左到右的習慣方式,那主要的方式就可以分爲先序遍歷、中序遍歷、後序遍歷。這裏的先中後是訪問根結點的順序,都是從左到右的。需要注意的是要區別清楚訪問與遍歷。

還有很容易想到的按層序遍歷。上面的三種遍歷方法的最大好處是可以方便的利用遞歸的思想來實現遍歷算法,當然也可以不使用遞歸思想來實現。這三種遍歷方法在使用非遞歸算法實現時,共同之處在於用棧來保存先前走過的路徑,以便可以在訪問完子樹後利用棧中的信息,回退到當前節點的雙親節點,進行下一步操作。層序遍歷法一般不能使用遞歸算法來編寫代碼,而是使用一個循環隊列來進行處理,它是最直觀的遍歷算法

(1)前序遍歷:又叫先序遍歷、先根遍歷,簡稱爲DLR遍歷。若二就能樹爲空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。就是先根後左再右,根左右。如下圖的左圖所示,遍歷的順序爲ABDGHCEIF。可以看出前序遍歷是先根結點,再按從上往下的層序在左子樹遍歷,最後也是按從上往下的順序在右子樹遍歷

(2)中序遍歷:也稱中根次序遍歷,簡稱LDR遍歷。若樹爲空則空操作返回,否則從根結點開始(注意並不是先訪問根結點),中序遍歷根結點的左子樹,然後訪問根結點,最後中序遍歷右子樹。如上圖的右圖,遍歷的順序是GDHBAEICF。可以看出中序遍歷是先在左子樹找到最底層的左子結點開始,有根結點就顯示根結點,無則顯示右子結點;然後回到上層,如此循環直到根結點,顯示根結點後再來到右子樹的最底層左子結點開始類似操作

(3)後序遍歷:也稱後根次序遍歷,簡稱LRD遍歷。若樹爲空則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後訪問根結點。如下圖的左圖所示,遍歷順序爲GHDBIEFCA。可以看出後序遍歷是先在左子樹找到最底層的左子結點開始

(4)層序遍歷:規則是若樹爲空則空操作返回,否則從樹的第一層(也就是根結點開始訪問),從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。如上圖的右圖所示,遍歷順序爲ABCDEFGHI。

上面提到的四種遍歷方法,其實都是在把樹中的結點變成某種意義的線性序列,這就程序的實現帶來了好處。

 

3.9.2  前序遍歷方法

實現遍歷算法時採用遞歸,簡潔明瞭。假設有如下圖這樣的一棵二叉樹T。這棵樹已經用二叉鏈表結構存儲在內存當中。前序遍歷算法就是先訪問根結點,然後遍歷左子樹,最後遍歷右子樹,可記做根左右。

  1. 首先訪問根結點,根結點不爲空,所以執行打印(具體操作自行決定),這裏打印A;若根結點爲空則返回;最終打印結果爲A。
  2. 遍歷根結點A的左子樹,不爲空,打印B;再遍歷結點B的左子樹,不爲打,打印D;再遍歷結點D的左子樹,不爲空,打印H;再遍歷結點H的左子樹,爲空返回;再遍歷結點H的右子樹,不爲空,打印K;遍歷結點K的左子樹,爲空返回;遍歷結點K的右子樹,爲空返回;此時結點K的左右子結點都爲空,返回到結點K的上一層結點H,H的操作執行完畢了(結點H的遍歷只是打印結點K,已經執行了);再返回到結點H的上一層結點D,其操作也執行完畢了,返回到結點D的上一層結點B,遍歷結點B的右結點E,打印結點E,因爲結點E無左右子結點,返回結點B的上一層結點A,即返回到根結點;
  3. 遍歷根結點A的右子樹C,操作類似上一步,打印的結果依次是F、I、G、J。最終的結果爲ABDHKECFIGJ。結束。

從上面的操作可以看出,前序遍歷的是先訪問根結點;再訪問根結點的左子樹,按層序從上往依次打印左結點,直到沒有左子結點,則打印同層的右子結點,若沒有右子結點,則返回到上一層,一直返回到根結點這一層;最後開始遍歷根結點的右子樹,也是開始按層序不斷的打印其左子結點,直到沒有左子結點;然後打印同層的右子結點,若同層無右子結點則返回上一層;直到打印完成。

/**
     * 前序遍歷,使用非遞歸實現,使用棧
      * @param node
     */
    public void traversalPreOrder(TreeNode<T> node) {
        Stack<TreeNode<T>> stack = new Stack<>();

        while (null != node || !stack.isEmpty()) {
            while (null != node) {
                System.out.println(node.data);
                stack.push(node);

                node = node.left;
            }
            if (!stack.isEmpty()) {
                node = stack.pop();
                node = node.right;
            }
        }
    }


    /**
     * 前序遍歷,使用遞歸實現
     * @param node
     */
    public void traversalPreOrderScan(TreeNode<T> node) {
        // 先根
        if (null != node) {
            System.out.println(node.data + " ");
        }
        // 中左
        TreeNode<T> left = node.left;
        if (null != left) {
            traversalPreOrderScan(left);
        }
        // 後右
        TreeNode<T> right = node.right;
        if (null != right) {
            traversalPreOrderScan(right);
        }
    }

再來看一個簡單的二叉樹,如下圖。前序遍歷結果爲ABDECF。

3.9.3 中序遍歷方法

首先遍歷左子樹,然後訪問根結點,最後遍歷右子樹,可記做左根右。中序遍歷算法的空間複雜度均爲O(n),時間複雜度爲n。還是上面的二叉樹,看看是如何執行的。

  1. 調用中序遍歷函數,因爲傳入的二叉樹T不爲空,所以訪問二叉樹的左子樹結點B;結點B的左子結點D不爲空,繼續訪問結點D;結點D的左子結點H不爲空,繼續訪問H;結點H的左子結點爲空,於是返回,並打印當前結點H;此步操作的打印結果爲H;
  2. 再來訪問結點的右子結點,發現存在結點K,再訪問結點K發現其無子結點,所以打印結點K;打印結點爲HK;
  3. 因爲結點K沒有子結點,返回;結點H已經打印,返回到H結點的上一層結點D;打印D;打印結點爲HKD;
  4. 結點D無右子結點,返回;打印B;因爲結點B有右子結點E,打印結點E;打印結點爲HKDBE;
  5. 結點E無右子結點,返回; 打印根結點A,打印A;打印結點爲HKDBEA;
  6. 到這裏二叉樹的左子樹打針完成,根結點A也已經訪問了,下面繼續打印二叉樹的右子樹,操作與上面類似,最終的結果爲HKDBEAIFCGJ。

從上面可以看出,中序遍歷是從根結點開始,先訪問左子樹。按層序從上往下一直訪問到左子樹的最一個左子結點纔開始打印,因爲最開始打印的最大層序的左子結點,若該左子結點有右子結點則打印右子結點;沒有則返回到上一層,有右子結點則打印,沒有則返回到上一層...,直到返回到根結點,打印根結點;最後是遍歷右子樹。打印右子樹時,也是按層序從上往下先打印最底層的左子結點(右子樹的左子結點)。

/**
     * 中序遍歷,使用遞歸
     * @param node
     */
    public void traversalInOrder(TreeNode<T> node) {
        if (null == node) {
            return;
        } else {
            // 先左
            traversalInOrder(node.left);
            // 中根
            System.out.println(node.data);
            // 後右
            traversalInOrder(node.right);
        }
    }

再來看一個簡單的二叉樹,如下圖。中序遍歷結果爲DBEAFC。

3.9.4 後序遍歷方法

它是先遍歷左子樹,再遍歷右子樹,最後訪問根結點,可記爲左右根。此遍歷方法是三種順序中最複雜的,原因在於它是先訪問左右子樹再訪問根結點,而在非遞歸算法中,利用棧回退時,並不知道是從左子樹回退到根節點還是從右子樹回退到根結點。若從右子樹回退到根結點,此時就應該去訪問右子樹,而若從右子樹回退到根結點,此時就應該去訪問根結點。所以相比前序和中序,必須得在壓棧時添加信息,以便在退棧時可以知道是從左子樹返回,還是從右子樹返回而進行下一步操作。

還是以上面的那棵二叉樹例。先遍歷左子樹,再遍歷右子樹,最後訪問根結點。

  1. 從左子樹按層序從上往下遍歷,到最後一層的根結點H,結點H沒有左子結點,但有右子結點K,結點K無子結點,所以打印K,返回;此步操作打印的結果爲K;
  2. 因爲上一層已經返回到了結果H這一層,結點K的子左右已經打印完了,所以打印結果H;到這裏的打印結果爲KH;
  3. 返回到上一層結點D,打印結點D;最終結果爲KHD;
  4. 返回到上一層結點B,結點B的左子結點已經打印了,在右子結點E,打印E;再打印B;最終結果爲KHDEB;
  5. 返回到根結點A,因爲根結點最後才執行,所以來到二叉樹的右子結點C,按層序到最下面一層的結點I,結點I沒有子結點,打印I;最終結點爲KHBEBI;
  6. 返回上一層到結點F,結點F無右子結點,打印F;最終結點爲KHBEBIF;
  7. 返回上一層來到結點C,因爲結點C有右子結點,所以這裏先不打印C,而是按層序到最後一層,來到結點J,結點J沒有子結點,所以打印J;最終結果爲KHDEBIFJ;
  8. 返回到上一層,來到結點G,結點G的子結點已經打印結束,所以打印G;最終結果爲KHDEBIFJG;
  9. 返回到上一層,來到結點C,結點C的子結點都已經打印,所以打印結點C;
  10. 最後返回到根結點A,打印A;最終結果爲KHDEBIFJGCA。
/**
     * 後序遍歷,使用遞歸算法
     * @param node
     */
    public void traversalPostorder(TreeNode<T> node) {
        if (null == node) {
            return;
        } else {
            // 先左
            traversalPostorder(node.left);
            // 中右
            traversalPostorder(node.right);
            // 後根結點
            System.out.println(node.data);
        }
    }

再來看一個簡單的二叉樹,如下圖。後序遍歷結果爲DEBFCA。

3.9.5 層序遍歷方法

由於二叉樹代表的一種層次結構,可能首先想到的便是按層來遍歷。對於二叉樹的按層遍歷,一般不能使用遞歸算法來編寫代碼,而是使用一個循環隊列來進行處理。首先將第一層(根結點)入隊,再將第根結點的左右子樹(第二層)入隊,直到所有的層都入隊。就是逐層遍歷。此遍歷方法是最直觀的遍歷算法。

/**
     * 按層遍歷法,即從第一層開始遍歷,到最後一層
     */
    public void traversalByLevel (TreeNode<T> node) {
        /**
         * 隊首和隊尾
         */
        int head = 0, tail = 0;
        /**
         * 先定義一個順序棧
         */
        TreeNode<T> p;
        TreeNode<T>[] n = new TreeNode[MAXLENGTH];

        /**
         * 若隊首引用不爲空,則設置隊尾
         */
        if (null != node) {
            tail = (tail + 1) % MAXLENGTH;
            n[tail] = node;
        }
        /**
         * 若隊列不爲空則循環
         */
        while (head != tail) {
            // 計算循環隊列的隊首序號
            head = (head + 1) % MAXLENGTH;
            // 獲取隊首元素
            p = n[head];
            // 處理隊首元素
            data(p);

            /**
             * 若存在左子樹
             */
            if (null != p.left) {
                // 計算循環隊列的隊尾序號
                tail = (tail + 1) % MAXLENGTH;
                // 將左子樹引用到隊列中
                n[tail] = p.left;
            }

            /**
             *  若存在右子樹
             */
            if (null != p.right) {
                tail = (tail + 1) % MAXLENGTH;
                n[tail] = p.right;
            }
        }
    }

3.9.6 推導遍歷結果

上面介紹了前中後序遍歷方法和層序遍歷方法,下面有一個問題:知道一棵二就能樹的前序遍歷序列爲ABCDEF,中序遍歷結點爲CBAEDF,問這棵二叉樹的後序遍歷結果是多少?

前序遍歷是先訪問根結點,所以這棵二叉樹的根結點爲A。中序結果爲CBAEDF,是先遍歷根結點的左子樹再訪問的根結點,由此可知根結點A的左子樹爲CB,EDF爲根結點A的右子樹。如下圖所示。

對於前序結果,先打印的B後打印的C,所以B是A的左子結點(因爲是從到右的順序,若B是A的右子結點就不會打印B);對於中序結點中的CB,先打印的C,所以C可能是B的左子結點或右子結點,但具體是右子結點還是左子結點目前尚不確定。

再看中序結果CBAEDF的EDF,那說明結點D爲結點A的右子結點,結點E和結點F分別爲結點D的左右兩個子結點。當然還有一種情況:三個結點EDF有三層,每層只有一個結點,即結點A下面是結點F,結點F下面是結點D,結點D下面是結點E,但若是這樣的話,那不會符合前序結果ABCDEF,所以只能是這種情況:明結點D爲結點A的右子結點,結點E和結點F分別爲結點D的左右兩個子結點。

4.10 二叉樹的建立

上面介紹了二叉樹的一些常用操作,那如何在內存中生成一棵二叉鏈表的二叉樹呢?若要在內存中建立一個如下圖中左圖這樣的樹,爲了能讓每個結點確認是否有左右子結點,對它進行了擴展,變成了右圖的樣子。也就是將二叉樹中每個結點的空指針引出一個虛結點,其值爲一個特定值,比如“#”。稱這種處理後的二叉樹爲原二叉樹的擴展二叉樹。擴展二叉樹就可以做到一個遍歷序列確定一棵二叉樹了。如下圖右圖的二叉樹的前序遍歷序列就爲AB#D##C##。

有了上面的準備後,來看看如何生成一棵二叉樹。假設二叉樹的結點均爲一個字符,把剛纔前序遍歷序列AB#D##C##用鍵盤挨個輸入。實現的算法如下:

               

 建立二叉樹,也是利用了遞歸的原理。當然,也完全可以用中序或後序遍歷的方式實現二叉樹的建立,只不過代碼裏生成結點和構造左右子樹的代碼順序要交換一下。另外,輸入的字符也要做相應的更改。上面擴展二叉樹的中序遍歷字符串應該爲#B#D#A#C#,後序遍歷序列爲###DB##CA。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章