數據結構之二叉樹(BinaryTree)

導讀

  二叉樹是一種很常見的數據結構,但要注意的是,二叉樹並非樹的特殊狀況,二叉樹與樹是兩種不同的數據結構。java

目錄

   一、 二叉樹的定義算法

  2、二叉樹爲什麼不是特殊的樹數組

  3、二叉樹的五種基本形態緩存

  4、二叉樹相關術語數據結構

  5、二叉樹的主要性質(6個)app

  6、二叉樹的存儲結構(2種)函數

  7、二叉樹的遍歷算法(4種)測試

  8、二叉樹的基本應用:二叉排序樹、平衡二叉樹、赫夫曼樹及赫夫曼編碼this

1、二叉樹的定義

  若是你知道樹的定義(有限個結點組成的具備層次關係的集合),那麼就很好理解二叉樹了。定義:二叉樹是n(n≥0)個結點的有限集,二叉樹是每一個結點最多有兩個子樹的樹結構,它由一個根結點及左子樹和右子樹組成。(這裏的左子樹和右子樹也是二叉樹)。編碼

  值得注意的是,二叉樹和「度至多爲2的有序樹」幾乎同樣,但,二叉樹不是樹的特殊情形。具體分析以下

2、二叉樹爲什麼不是特殊的樹

  一、二叉樹與無序樹不一樣

  二叉樹的子樹有左右之分,不能顛倒。無序樹的子樹無左右之分。

  二、二叉樹與有序樹也不一樣(關鍵)

  當有序樹有兩個子樹時,確實能夠看作一顆二叉樹,但當只有一個子樹時,就沒有了左右之分,如圖所示:

  

3、二叉樹的五種基本狀態

  

4、二叉樹相關術語

  滿二叉樹:全部葉子結點所有集中在最後一層,這樣的二叉樹稱爲滿二叉樹。(注意:國內的定義是每一層的結點都達到最大值時纔算是滿二叉樹;而國際定義爲,不存在度爲1的結點,即結點的度要麼爲2要麼爲0,這樣的二叉樹就稱爲滿二叉樹。這兩種概念徹底不一樣,既然在國內,咱們就默認第一種定義就好)。

  徹底二叉樹:若是將一顆深度爲K的二叉樹按從上到下、從左到右的順序進行編號,若是各結點的編號與深度爲K的滿二叉樹相同位置的編號徹底對應,那麼這就是一顆徹底二叉樹。如圖所示:

  

5、二叉樹的主要性質

  二叉樹的性質是基於它的結構而得來的,這些性質沒必要死記,使用到再查詢或者本身根據二叉樹結構進行推理便可。

  性質1:非空二叉樹的葉子結點數等於雙分支結點數加1。

  證實:設二叉樹的葉子結點數爲X,單分支結點數爲Y,雙分支結點數爲Z。則總結點數=X+Y+Z總分支數=Y+2Z

     因爲二叉樹除了根結點外其餘結點都有惟一的分支指向它,因此總分支數=總結點數-1.。

     結合三個方程:總分支數=總結點數-1,即Y+2Z = X+Y+Z-1。化簡獲得X = Z + 1。即葉子結點數等於雙分支結點數加1。

  性質2:在二叉樹的第i層上最多有2 i-1 個結點 (i>=1)。

  證實:二叉樹結點最多的狀況即爲滿二叉樹的狀況,因爲是滿二叉樹,每一個結點都有兩個孩子,因此下一層是上一層的2倍,構成了公比爲2的等比數列,而第一層只有根結點,因此首項是1。因此二叉樹的第i層上最多有2 i-1 個結點。

  性質3:高度(或深度)爲K的二叉樹最多有2k - 1個結點(K>=1)。

  證實:本性質其實就是性質2中描述的等比數列的前項和的問題。

  性質4:若對含 n 個結點的徹底二叉樹從上到下且從左至右進行 1 至 n 的編號,則對徹底二叉樹中任意一個編號爲 i 的結點:

  (1) 若 i=1,則該結點是二叉樹的根,無雙親, 不然,編號爲 [i/2] 的結點爲其雙親結點;  

  (2) 若 2i>n,則該結點無左孩子,  不然,編號爲 2i 的結點爲其左孩子結點;

  (3) 若 2i+1>n,則該結點無右孩子結點,  不然,編號爲2i+1 的結點爲其右孩子結點。

          

  性質5:具備n個結點的徹底二叉樹的深度爲[log2n]+1或者[log2(n+1)],其中[log2n]+1是向下取整,[log2(n+1)]是向上取整。

    性質6:Catalan函數性質:給定n個結點,能構成H(n)種結構不一樣的樹。H(n) = c(2n,n) / (n+1)。

6、二叉樹的存儲結構

  爲了方便說明,咱們使用下圖樹1做爲案例樹。

  

一、順序存儲

  順序存儲是使用一個數組來存儲二叉樹,咱們通常將二叉樹按照性質4的作法,即從上到下且從左至右進行 1 至 n 的編號,而後編號與數組下標對應,按照編號依次將對應的結點信息存儲數組中便可。

  第一步:給二叉樹編號

  

   注意,編號5的位置是沒有結點的,可是咱們這樣編號的目的是爲了更好的應用性質4,而性質4描述的是徹底二叉樹,因此咱們編號時要將普通二叉樹看作徹底二叉樹來進行編號,因此即便編號5即便沒有結點也須要進行編號。

  第二步:按編號存儲到數組BTree[]中

 

  第三步:按照性質4的規律取元素

  性質4主要是描述結點的編號和雙親編號或孩子編號之間的數學關係,即咱們知道了一個結點的編號,那麼它的雙親編號和孩子孩子咱們都能計算獲得並從數組中取出來。

  例如,結合性質4咱們來計算編號3的雙親和孩子:選取出編號3即BTree[3],就能夠知道編號3的元素爲C;雙親結點編號 = 3/2 = 1 ≥ 1,因此C結點的雙親爲BTree[1]即A;左孩子編號 = 3*2 = 6 ≤ 7,因此C的右孩子爲BTree[6] = E;右孩子編號 = 3*2+1 = 7 ≤ 7,因此C的右孩子爲Btree[7] = F。

  結論:像編號5這種狀況會佔用存儲空間,因此這種存儲方式最適合用於存儲徹底二叉樹,而存儲通常的二叉樹則會浪費大量空間。

二、鏈式存儲

  根據二叉樹的結構,咱們使用下面的鏈式結點來存儲一個二叉樹結點。

  

  因此樹1對應的鏈式存儲結構爲:

  

7、二叉樹的遍歷算法

  根據二叉樹的結構特色:通常二叉樹由左子樹、根結點和右子樹組成。這三個元素:左子樹(L)、根結點(N)、右子樹(R)有6種中排列組合,即NLR、LNR、LRN、NRL、RNL、RLN。而從左往右和從右往左這種遍歷順序是對稱結構的,採用一種順序便可,因此二叉樹按照三個元素的排列順序遍歷就造成了:NLR(先序遍歷)、LNR(中序遍歷)和LRN(後序遍歷)。

  ps:二叉樹的這三種遍歷要用遞歸的思想去理解。

  先序遍歷(NLR):根左右

  1)訪問根結點

  2)先序遍歷左子樹

  3)先序遍歷右子樹

  中序遍歷(LNR):左根右

  1)中序遍歷左子樹

  2)訪問根結點

  3)中序遍歷右子樹

  後序遍歷(LRN):左右根

  1)後序遍歷左子樹

  2)後序遍歷右子樹

  3)訪問根結點

  java實現(遍歷樹1):

 1 package test;
 2 
 3 import org.junit.Test;
 4 
 5 /**
 6  * 二叉樹的遍歷
 7  * @author Fzz
 8  * @date 2018年1月17日
 9  * @Description TODO:
10  */
11 public class BinaryTreeTraversal {
12     private StringBuffer sb = new StringBuffer();
13     //先序遍歷(數組)
14     public String first(Object[] o,int i){
15         //訪問根結點
16         sb.append(o[i]);
17         //遍歷左子樹
18         int left = i*2;
19         if(left<o.length&&o[left]!=null)
20             first(o,left);
21         //遍歷右子樹
22         int right = i*2+1;
23         if(right<o.length&&o[right]!=null)
24             first(o,right);
25         return sb.toString();
26     }
27     
28     //中序遍歷
29     public String mid(Object[] o,int i){
30         //遍歷左子樹
31         int left = i*2;
32         if(left<o.length&&o[left]!=null)
33             mid(o,left);
34         //訪問根結點
35         sb.append(o[i]);
36         //遍歷右子樹
37         int right = i*2+1;
38         if(right<o.length&&o[right]!=null)
39             mid(o,right);
40         return sb.toString();
41     }
42     
43     //後序遍歷
44     public String last(Object[] o,int i){
45         //遍歷左子樹
46         int left = i*2;
47         if(left<o.length&&o[left]!=null)
48             last(o,left);
49         //遍歷右子樹
50         int right = i*2+1;
51         if(right<o.length&&o[right]!=null)
52             last(o,right);
53         //訪問根結點
54         sb.append(o[i]);
55         return sb.toString();
56     }
57     
58     //將緩存區設爲空
59     public void setBufferNull(){
60         this.sb = this.sb.delete(0, sb.length());
61     }
62     
63     @Test
64     public void test(){
65         Character[] o = {null,'A','B','C','D',null,'E','F'};
66         //遍歷前先清空緩存區
67         this.setBufferNull();
68         String s = first(o,1);
69         System.out.println("先序遍歷結果:"+s);
70         this.setBufferNull();
71         s = mid(o,1);
72         System.out.println("中序遍歷結果:"+s);
73         this.setBufferNull();
74         s = last(o,1);
75         System.out.println("後序遍歷結果:"+s);
76     }
77 }

  測試結果:

  

  層次遍歷

  層次遍歷比較簡單,即按照從上往下、從左往右一層一層遍歷便可。層次遍歷是現實,若是遍歷的是順序存儲(數組存儲)的二叉樹,因爲存儲的時候就是按照從上往下從左往右的順序存儲的,直接按順序取出便可;若是是鏈式存儲的二叉樹,須要使用一個循環隊列進行操做:先將根結點入隊,當前結點是隊頭結點,將其出隊並訪問,若是當前結點的左結點不爲空將左結點入隊,若是當前結點的右結點不爲空將其入隊。因此出隊順序也是從左到右依次出隊。

8、二叉樹的基本應用

一、二叉排序樹(Binary Sort Tree)

  二叉排序樹,又稱二叉查找樹(Binary Search Tree),亦稱二叉搜索樹。

  一、定義

  二叉排序樹或者是一棵空樹,或者是具備下列性質的二叉樹:

  (1)若左子樹不空,則左子樹上全部結點的值均小於或等於它的根結點的值;
  (2)若右子樹不空,則右子樹上全部結點的值均大於或等於它的根結點的值;
  (3)左、右子樹也分別爲二叉排序樹;
  ps:根據二叉排序樹的定義,若是對二叉排序樹進行中序遍歷,那麼遍歷的結果就是一個遞增的序列。
  二、查找關鍵字
  二叉排序樹的主要功能就是查找。首先將須要查找的序列排序後存儲到二叉排序樹中,那麼要查找的關鍵字要麼在左子樹,要麼在根結點,要麼在右子樹,因此咱們首先將要查找的關鍵字與根結點作比較,相等則查找成功。小於根結點則遞歸查找左子樹,大於根結點則遞歸查找右子樹,直到出現相等狀況則查找成功,不然查找失敗。(該查找過程與折半查找相似)
  三、插入關鍵字
  插入操做主要是對查找不成功的排序二叉樹,即若是關鍵字查找不成功,那麼咱們就須要將查找不成功的關鍵字插入查找不成功的位置。因此咱們只須要將查找算法進行修改就能實現插入操做(ps:若二叉樹爲空,則首先單獨生成根結點):
 1 BiTree* InsertBST(BiTree *t,int key)
 2 {
 3     if (t == NULL)
 4     {
 5         t = new BiTree();
 6         t->lchild = t->rchild = NULL;
 7         t->data = key;
 8         return t;
 9     }
10  
11     if (key < t->data) 
12         t->lchild = InsertBST(t->lchild, key);
13     else
14         t->rchild = InsertBST(t->rchild, key);
15  
16     return t;
17 }
  四、刪除關鍵字
  在刪除關鍵字結點時,須要注意的是,在刪除後咱們須要保持二叉排序樹的特性。
  在二叉排序樹刪去一個結點,分三種狀況討論:(設被刪除結點爲p,p的雙親結點爲f)
  1. 若p結點爲葉子結點,即PL(左子樹)和PR(右子樹)均爲空樹。因爲刪去葉子結點不破壞整棵樹的結構,則能夠 直接刪除此子結點。
  2. 若p結點只有左子樹PL或右子樹PR,此時只要令PL或PR直接成爲其雙親結點f的左子樹(當p是左子樹)或右子樹(當p是右子樹)便可,做此修改也不破壞二叉排序樹的特性。
  3. 若p結點的左子樹和右子樹均不空。這種狀況能夠轉換爲狀況1或2而後再按照1或2的方法來解決。有兩種轉換方法:
    其一是找到p結點的左子樹的最右邊的結點r(沿着p的左子樹的根結點的右指針一直走,其實就是找到左子樹的最大值),用r結點替換p結點,而後再刪除r結點便可。
    其二是找到p結點的右子樹的最左邊的結點r(沿着p的右子樹的根結點的左指針一直走,其實就是找到左子樹的最小值),用r結點替換p結點,而後再刪除r結點便可。
    java實現:
private void deleteNode(BinarySortTree p)
{
    //TODOAuto-generatedmethodstub
    if(p!=null)
    {
        //若是結點有左子樹
        /*1。若p有左子樹,找到其左子樹的最右邊的葉子結點r,用該葉子結點r來替代p,把r的左孩子
        做爲r的父親的右孩子。
        2。若p沒有左子樹,直接用p的右孩子取代它。
        */
        if(p.lChild!=null)
        {
            BinarySortTree r=p.lChild;
            BinarySortTree prev=p.lChild;
                while(r.rChild!=null)
               {
                    prev=r;
                    r=r.rChild;
                }
                p.data=r.data;
            //若r不是p的左子樹,p的左子樹不變,r的左子樹做爲r的父結點的右孩子結點
            if(prev!=r)
            {
                prev.rChild=r.lChild;
             }
            else
            {
             //若r是p的左子樹,則p的左子樹指向r的左子樹
            p.lChild=r.lChild;
            }
        }
        else
        {
            p=p.rChild;
        }
    }
}

二、平衡二叉樹

  平衡二叉樹又稱爲AVL樹,是一種特殊的二叉排序樹,即左右兩個子樹高度之差不超過1,而且左右兩個子樹都是平衡二叉樹的二叉排序樹稱爲平衡二叉樹。

  爲何要構造平衡二叉樹呢?對於通常的二叉排序樹,其指望高度(即爲一棵平衡樹時)爲log2n,其各操做的時間複雜度(O(log2n))同時也由此而決定。因爲AVL樹的左右子樹高度之差不超過1,其高度通常都良好地維持在O(log(n)),大大下降了操做的時間複雜度

  平衡二叉樹的實現算法:關鍵在於左右子樹的平衡。具體的算法有:紅黑樹、AVL算法、Treap、伸展樹、SBT來實現。有興趣的能夠自行搜索,這裏就再也不描述。

三、赫夫曼樹及赫夫曼編碼

  赫夫曼樹又叫作最優二叉樹,特色是帶權路徑長度最短

  一、相關術語

  路徑和路徑長度:在一棵樹中,從一個結點往下能夠達到的孩子或孫子結點之間的通路,稱爲路徑。通路中分支的數目稱爲路徑長度
  結點的權及帶權路徑長度:若將樹中結點賦給一個有着某種含義的數值,則這個數值稱爲該結點的權。結點的帶權路徑長度爲:從根結點到該結點之間的路徑長度與該結點的權的乘積
  樹的帶權路徑長度:樹的帶權路徑長度規定爲全部葉子結點的帶權路徑長度之和,記爲WPL。
  以下圖有4個葉子結點的二叉樹:
  
   則這棵樹的WPL = 2*7+2*5+3*2+2*4 = 38。
  二、赫夫曼樹的構造
  假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設爲 w一、w二、…、wn,則哈夫曼樹的構造規則爲:
  (1) 將w一、w二、…,wn當作是有n 棵樹的森林(每棵樹僅有一個結點);
  (2) 在森林中選出兩個根結點的權值最小的樹合併,做爲一棵新樹的左、右子樹,且新樹的根結點權值爲其左、右子樹根結點權值之和;
  (3)從森林中刪除選取的兩棵樹,並將新樹加入森林;
  (4)重複(2)、(3)步,直到森林中只剩一棵樹爲止,該樹即爲所求得的哈夫曼樹。
  以上面的二叉樹爲例:首先有4個葉子結點a、b、c、d,權值分別爲七、五、二、4。則:
   普構造獲得的赫夫曼樹的帶權路徑長度是最小的,WPL = 1*7+2*5+3*2+4*4 = 35。
  三、赫夫曼樹的特色
  1)權值越大的結點離根結點越近。
  2)樹中沒有度爲1的結點。這類樹又稱爲正則(嚴格)二叉樹。
  四、赫夫曼樹的應用:赫夫曼編碼
  赫夫曼編碼是一種編碼方式。例如咱們須要發送右A、B、C、D這4個字符組成的一些文字,若是分別用00,01,10,11來表明要傳送的A、B、C、D。則須要發送"ADA"就編碼爲:001100。
  咱們但願編碼的長度可以越短越好,上面的編碼方式長度顯然是與字符個數成正比的,不能知足需求,因此赫夫曼想到了設置不一樣的編碼長度來表達不一樣字符,那麼讓出現機率越高的字符設置編碼長度越短便可。假設A、B、C、D出現的機率分別爲0.四、0.三、0.二、0.1。那麼A、B、C、D的編碼就能夠分別用0、00、0一、1來表示。那麼ADA的編碼就是「010」,但"CA"的編碼也是「010」,因此咱們在設置不一樣長度的編碼時須要使用前綴編碼(即任一編碼都不是其餘編碼的前綴,這樣就不會產生歧義了)。因此A、B、C、D的編碼修改成:0、十、1十、111便可。
  赫夫曼編碼就是長度可變(編碼長度最短)的前綴編碼。其實,赫夫曼編碼的構造就是赫夫曼樹的構造過程:即字符至關於葉子結點;字符出現的機率至關於結點的權值;因爲構造的赫夫曼樹權值越大的結點離根結點越近,因此字符出現機率越大的字符離根結點越近,路徑也就越短。
  因此,A、B、C、D(0.四、0.三、0.二、0.1)構造的赫夫曼樹過程以下:
  1)構造赫夫曼樹。
  2)約定左分支表示0,右分支表示1。
  3)從根結點到字符結點的路徑組成的編碼就是該字符的赫夫曼編碼。
  
相關文章
相關標籤/搜索