1. 程式人生 > >[從今天開始修煉資料結構]樹,二叉樹,線索二叉樹,霍夫曼樹

[從今天開始修煉資料結構]樹,二叉樹,線索二叉樹,霍夫曼樹

前面我們已經提到了線性表,棧,佇列等資料結構,他們有一個共同的特性,就是結構中每一個元素都是一對一的,可是在現實中,還有很多一對多的情況需要處理,所以我們需要研究這種一對多的資料結構 —— 樹,並運用它的特性來解決我們在程式設計中遇到的問題。

一、樹的定義

  1,樹Tree是n(n >= 0) 個結點的有限集。n = 0時稱為空樹  在任意一棵非空的樹中,

  (1)有且僅有一個特定的根結點

  (2)當n>1時,其餘節點可分為m(m > 0)個互不相交的有限集T1,T2,.....,Tm,其中每一個集合又是一棵樹,並且稱為根的子樹。如下圖所示

 

注意:1,根節點是唯一的    2,子樹的個數沒有限制,但它們一定是互不相交的

2,結點分類

 

  (1)結點包含一個數據元素以及若干指向其子樹的分支。

  (2)結點擁有的子樹數量稱為結點的度

  (3)度為0的結點稱為葉節點或終端節點

  (4)度不為0的結點稱為非終端結點或分支結點

  (5)一棵樹的度是樹內各節點的度的最大值

3,結點間的關係

  結點的子樹的根稱為該結點的孩子,該結點稱為孩子的雙親。同一個雙親的孩子之間互稱兄弟。結點的祖先是從根到該結點所經分支上的所有結點。反之,以某結點為根的子樹中的任一結點都成為該結點的子孫。

4,樹的其他相關概念

  (1)結點的層次從根開始定義,根為第一層,根的孩子為第二層,以此類推。

  (2)其雙親在同一層的結點互為表兄弟

  (3)樹中結點的最大層次稱為樹的深度或高度

  (4)如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。

  (5)森林是m(m >= 0)棵互不相交的樹的集合

二、樹的儲存結構

  我們介紹三種不同的表示方法:雙親表示法、孩子表示法、孩子兄弟表示法。

  1,雙親表示法

  結點可能沒有孩子,但一定有雙親。假設我們以一組連續空間儲存樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點在陣列中的位置。由於根節點沒有雙親,所以我們約定根節點的位置域設為-1.下面是示例

下標 data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 2
5 F 2
6 G 3
7 H 3
8 I 3
9 J 4

這樣的儲存結構,我們可以根據結點的parent指標很容易找到雙親,時間複雜度O(1)。但如果我們要知道結點的孩子呢?對不起,請遍歷整個結構才行。那麼能不能改進一下呢?

我們增加一個結點最左邊孩子的域,不妨叫他長子域,這樣很容易得到結點的孩子。如果沒有孩子的葉結點,這個長子域就設為-1,如下表

下標 data parent firstchild
0 A -1 1
1 B 0 3
2 C 0 4
3 D 1 6
4 E 2 9
5 F 2 -1
6 G 3 -1
7 H 3 -1
8 I 3 -1
9 J 4 -1

另外一個問題場景,我們關注各兄弟之間的關係,雙親表示法無法體現這樣的關係,怎麼辦呢?可以增加一個右兄弟域來體現兄弟關係,也就是說,每一個結點如果它存在右兄弟,就記錄下右兄弟的下標,同樣的如果右兄弟不存在,就賦值為-1.

下標 data parent rightsib
0 A -1 -1
1 B 0 2
2 C 0 -1
3 D 1 -1
4 E 2 5
5 F 2 -1
6 G 3 7
7 H 3 8
8 I 3 -1
9 J 4 -1

如果節點的孩子很多,超過了兩個,我們又關注節點的雙親,又關注節點的孩子,還關注結點的兄弟,而且還對時間遍歷要求高,那麼我們可以把此結構擴充套件為有各個域都包含。儲存結構的設計是一個非常靈活的過程,一個儲存結構設計的是否合理,取決於基於改儲存結構的運算是否適合、是否方便,時間複雜度好不好等。

2,孩子表示法

 現在我們換一種不同的考慮方法。由於樹中每個結點可能有多棵子樹,可以考慮用多重連結串列即每個節點有多個指標域,其中每個指標指向一棵子樹的根節點,我們把這種方法叫做多重連結串列表示法。

不過,樹的每個結點的度,也就是孩子的個數是不同的,所以設計兩種方案來解決。

方案一

  第一種方案是指標域的個數等於樹的度。

 

 

   對於上面作為示例的樹來說,度是3

 

 

 這種方法對於樹中結點度相差很大時,是浪費空間的。

方案二

  每個結點指標域的個數等於該結點的度,專門取宇哥位置來儲存結點指標域的個數,結構如下

 

 

 這種方案克服了空間浪費的缺陷,但由於每個結點的連結串列不同,加上要維護結點的度的值,在運算上會有時間上的損耗。

為了同時滿足空間不浪費,又使節點結構相同,我們引出 —— 孩子表示法。 把每個節點的孩子結點排列起來,以單鏈表作為儲存結構,則n個結點有n個孩子連結串列,如果是葉子結點則此單鏈表為空。然後n個頭指標又組成一個線性表,採用順序儲存結構,存放進一個一維陣列。為了快速找到某個結點的雙親,我們把雙親表示法和孩子表示法綜合一下如下圖。

 

 

 這種方法叫做雙親孩子表示法,是對孩子表示法的改進。結構定義如下

public class ParentChildDemo <T>{
    
    PCTBox[] nodes; //表頭陣列
    int r;  //根節點的角標
    int n;  //總結點樹
    int parent;  //雙親結點的角標,根節點為-1
    
    
    private class PCTBox<T>{
        T data;
        ChildNode firstchild;
    }
    private class ChildNode{
        int child; //儲存本結點在表頭陣列中的下標
        ChildNode next;
    }
}

 

3,孩子兄弟表示法 

  任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此我們設定兩個指標,分別指向該結點的第一個孩子和此節點的右兄弟。

  

 

 

 如果有必要完全可以再增加一個parent指標域來解決快速查詢雙親的問題。這個表示法最大的好處就是它把一棵複雜的樹變成了一棵二叉樹。

三、二叉樹     重點來了!!

  對於在某個階段都是兩種結果的情形,比如開和關,0和1,真和假,上和下,正與反等,都適合用二叉樹來表示

  1,二叉樹的定義

    二叉樹 Binary Tree 是n個結點的有限集合,該集合或者為空集,或者由一個根節點和兩棵互不相交的、分別稱為根節點的左子樹和右子樹的二叉樹組成。形如下圖

 

 

 2,二叉樹的特點: 

  (1)每個結點最多有兩棵子樹

  (2)左子樹和右子樹有順序,不能顛倒

  (3)如果某結點只有一棵子樹,那也要區分它是左子樹還是右子樹。

3,特殊二叉樹

  (1)斜樹。所有結點都只有左子樹的二叉樹叫做左斜樹。所有結點都是隻有右子樹的二叉樹叫右斜樹。

  (2)滿二叉樹。 所有分支結點都存在左子樹和右子樹,並且所有葉子都在同一層上,這樣的二叉樹稱為滿二叉樹。

  (3)完全二叉樹。對一棵具有n個結點的二叉樹按照層序編號,如果編號為i的結點與同樣深度的滿二叉樹中編號為i的結點在二叉樹中的位置完全相同,則這棵二叉樹稱為完全二叉樹。簡單來說就是最後一個結點的之前結點是按照滿來排列的,沒有空檔。矮的子樹一定是右子樹,且比左子樹最多矮一層。最下層的葉子一定集中在左部連續位置。同樣結點數的二叉樹,完全二叉樹的深度最小。

4,二叉樹的性質

  (1)在二叉樹的第i層上至多有2i-1個結點。

  (2)深度為k的二叉樹至多有2k-1個結點

  (3)對任何一棵二叉樹T,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1

  (4)具有n個結點的完全二叉樹的深度為。表示不大於x的最大整數。

 

 

  (5)如果對一棵有n個結點的完全二叉樹的結點按層序編號,對任意結點有:

      如果i=1,則i是二叉樹的根;如果i>1,則其雙親是

 

      如果2i>n,則結點i無左孩子(結點i是葉子結點);否則其左孩子是結點2i

      如果2i+1>n,則結點i無右孩子;否則其右孩子是2i+1

四、二叉樹的儲存結構

 

   1,順序儲存結構

    二叉樹是一種特殊的樹,用順序儲存結構可以實現。用一維陣列儲存二叉樹中的結點,並且節點的儲存位置,也就是陣列的下標,要能體現結點之間的邏輯關係。舉例如下

  

 

 

 將這棵樹存入陣列中(打叉的元素表示不存在)如下

 

 

   這種方式如果遇上右斜樹,那麼會浪費很多空間,所以順序儲存方式一般只用於完全二叉樹。

  2,二叉連結串列

  二叉樹每個結點最多有兩個孩子,所以為它設計一個數據域和兩個指標域,我們稱為二叉連結串列

 

 

 

public class BiTNode<T> {
    T data;
    BiTNode lchild;
    BiTNode rchild;
}

五、遍歷二叉樹

  1,二叉樹的遍歷是指從根節點出發,按照某種次序依次訪問二叉樹中的所有結點,使得每個結點被訪問一次且僅被訪問一次。

    二叉樹遍歷不同於線性結構,在一個結點訪問完成後面臨多個選擇。

  2,二叉樹遍歷方法

    (1)前序遍歷

    前序遍歷、中序遍歷等遍歷名稱的“前” “中”是指雙親結點和子結點訪問時雙親結點在前被訪問還是在中間被訪問

    若樹為空,則空操作返回;先訪問根節點,然後前序遍歷左子樹,再前序遍歷右子樹。(先父後子,先左後右,根左右)

     

 

 

 結果是ABDGHCEIF

(2)中序遍歷

  從根結點開始(但並不先訪問根結點),中序遍歷根節點的左子樹,然後訪問根節點,最後中序遍歷右子樹(左根右)

 

 

 結果是GDHBAEICF

 (3)後序遍歷

  若空,返回空操作;否則從左到右先葉子後結點的方式遍歷訪問左右子樹,然後訪問根節點。(左右根)

 

 

結果:GHDBIEFCA 

(4)層序遍歷

 若空,則空操作返回,否則從樹的第一層,也就是根節點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問

 

 

 結果:ABCDEFGHI

研究這些遍歷方法其實就是在把樹中的結點變成有意義的線性序列,通過不同的遍歷方法提供了對結點的不同處理方式。

首先是結點定義

    public static class Node<T>{
        private T data;
        private Node lChild;
        private Node rChild;

        public Node(T data) {
            this.data = data;
        }

        public void setNode(T data){
            this.data = data;
        }
    }

 

    /**
     * 前序遍歷的遞迴寫法
     */
    public void PreOrderTraverse1(Node root){
        if (root == null)
            return;
        System.out.println(root.data);
        PreOrderTraverse1(root.lChild);
        PreOrderTraverse1(root.rChild);
    }

    /**
     * 前序遍歷的非遞迴寫法
     * 遞迴轉成棧
     */
    public void PreOrderTraverse2(Node root){
        ArrayStack<Node> a = new ArrayStack<>();
        while (root != null || !a.isEmpty()){
            while (root != null){
                System.out.println(root.data);
                a.push(root);
                root = root.lChild;
            }

            if (!a.isEmpty()){
                root = a.pop();
                root = root.rChild;
            }
        }
    }

    /**
     * 中序遍歷的遞迴寫法
     */
    public void InOrderTraverse1(Node root){
        Node node = root;
        if (node == null){
            return;
        }
        InOrderTraverse1(node.lChild);
        System.out.println(node.data);
        InOrderTraverse1(node.rChild);
    }

    /**
     * 中序遍歷的非遞迴寫法
     */
    public void InOrderTraverse2(Node root){
        ArrayStack<Node> a = new ArrayStack<>();
        while (root != null || !a.isEmpty()){
            while (root.lChild != null){
                a.push(root);
                root = root.lChild;
            }
            if (!a.isEmpty()){
                root = a.pop();
                System.out.println(root.data);
                root = root.rChild;

            }
        }
    }

  /**
     * 後序遍歷的遞迴寫法
     */
    public void PostOrderTraverse1(Node root){
        if (root == null){
            return;
        }
        PostOrderTraverse1(root.lChild);
        PostOrderTraverse1(root.rChild);
        System.out.println(root.data);
    }

    /**
     * 後序遍歷的非遞迴寫法 —— 雙棧法
     * 將前序遍歷的中左右,調換變成左右中
     *
     */
    public void PostOrderTraverse2(Node root){
        ArrayStack<Node> a1 = new ArrayStack<>();
        ArrayStack<Node> a2 = new ArrayStack<>();
        Node r = root;
        while (r != null || !a1.isEmpty()){
            while (r != null){
                a1.push(r);
                a2.push(r);
                r = r.rChild;
            }
            if (!a1.isEmpty()){
                r = a1.pop();
                r = r.lChild;
            }
        }
        while (!a2.isEmpty()){
            r = a2.pop();
            System.out.println(r.data);
        }
    }

    /**
     * 利用佇列實現層序遍歷(可以不用優先佇列,這個優先佇列Demo是我前面隨筆寫的,順便拿來用用)
     */
    public void LevelOrderTraverse(Node root) throws Exception {
        PriorityQueueDemo<Node> p = new PriorityQueueDemo<>(10);
        Node t;
        p.add(root);
        while (p.size() != 0){
            t = p.poll();
            System.out.println(t.data);
            if (t.lChild != null)  p.add(t.lChild);
            if (t.rChild != null)  p.add(t.rChild);
        }

    }

 

二叉樹遍歷的兩個性質:  已知前序遍歷序列和中序遍歷序列,可以唯一確定一棵二叉樹

             已知後序遍歷序列和中序遍歷序列,可以唯一確定一棵二叉樹

六、二叉樹的建立

  我們要建立一棵如下左圖的樹,為了能讓每個結點確認是否有左右孩子,我們對它進行了擴充套件,變成如下右圖的樣子。我們稱右圖為左圖的擴充套件二叉樹。擴充套件二叉樹可以做到一個遍歷序列就確定一棵二叉樹。

 

 上右圖的前序遍歷結果為 AB#D##C##,我們把這樣的遍歷結果輸入下面程式中就可以建立左圖二叉樹了

    private Object[] results;
    public BiTree(Object[] arr){
        this.results = arr;
    }
    private static int index = 0;
    public Node<T> buildBiTree(){
        if (index >= results.length || results[index].equals('#')){
            index++;
            return null;
        }
        Node<T> node = new Node<T>((T)results[index++]);
        node.lChild = buildBiTree();
        node.rChild = buildBiTree();
        return node;
    }

 

七、線索二叉樹

  1,我們來看如下的鏈式二叉樹

 

 裡面右許多的空指標^沒有被利用起來,我們來計算一下,一個n個結點的二叉樹,有2n個指標域,n-1條分支線路,也就是說有2n-n+1=n+1個空指標域在浪費著。

另外,我們在遍歷時,知道上圖的中序遍歷結果是HDIBJEAFCG,此時我們可以知道比如D的前驅是H,後繼是I,但我們在沒有遍歷的情況下是不知道的。綜合以上兩點,我們可以利用那些空地址,存放結點在某種遍歷次序下的前驅和後繼結點的位置。

我們把這種指向前驅和後繼的指標稱為線索,加上線索的二叉連結串列稱為線索連結串列,相應的二叉樹稱為線索二叉樹(Threaded Binary Tree)

我們讓所有空閒的左指標指向前驅,所有空閒的右指標指向後繼;並且增加ltag域和rtag域區分左右指標指向的到底是孩子還是前驅後繼。

其中 ltag和rtag為0時指向的是左孩子或右孩子,為1時指向的是前驅或後繼。

  2,線索二叉樹的實現

  因為前驅和後繼只有在遍歷過程中才能拿到,所以實現線索二叉樹的過程實質上就是在遍歷過程中修改空指標的過程。

  

    private Node pre;//根節點的pre是head,head的rtag = 1public void Threading(Node root) {
        Node r = root;
        if (root == null){
            return;
        }else {
            Threading(r.lChild);
            if (r.lChild == null){
                r.ltag = 1;
                r.lChild = pre;
            }
            if ( pre != null&&pre.rChild == null){
                pre.rtag = 1;
                pre.rChild = r;
            }
            pre = r;
            Threading(r.rChild);
        }
    }

 

這個遞迴實現只不過是把中序遍歷中訪問結點資料的程式碼改成了修改指標的程式碼。

有了前驅後繼,我們就可以通過二叉線索樹來遍歷二叉樹了。對於中序遍歷來說,先查詢線索連結串列的第一個節點,也就是最左方向上的最後一個節點,然後如果有右線索先尋找後繼節點,查詢到斷線索(有右節點啦)就往下找一個右節點,繼續這樣摸下去,其實說到底就是有線索先按線索找(注意線索上的節點是需要訪問的),等到線索斷了就往下找右孩子節點(找到右孩子結點後按照中序遍歷先左孩子後雙親結點再右孩子的順序遍歷該子樹)。

    /**
     * 按照二叉線索樹的線索遍歷
     */
    public void InOrderTraverse_Threaded(Node root){
        Node r = root;
        while (r != null) {
            while (r.lChild != null && r.ltag == 0) {
                r = r.lChild;
            }
            System.out.println(r.data);
            while (r.rtag == 1) {
                r = r.rChild;
                System.out.println(r.data);
            }
            r = r.rChild;
        }
    }

可以看到如果所用的二叉樹需要經常遍歷或者需要某種遍歷序列中的前驅和後繼來查詢結點,那麼採用線索二叉連結串列的儲存結構是非常不錯的選擇。

八、樹,森林與二叉樹的轉換。

  1,樹轉化為二叉樹

  步驟: (1)加線。在所有的兄弟結點之間加一條線

     (2)去線。對樹中的每個結點,只保留它與第一個孩子結點的連線,刪除它與其他孩子結點的連線。

     (3)層次調整。以樹的根節點為軸線,將整棵樹順時針旋轉一定的角度,使之層次分明。  注:上面提到的第一個孩子是二叉樹的左孩子,第一個孩子的兄弟轉換過來的是二叉樹的右孩子。

 

 

   2,森林轉化為二叉樹

  把森林中的每一棵樹認為是兄弟,按照上面兄弟的處理辦法來操作。步驟如下:

  (1)把每棵樹轉化成二叉樹

  (2)第一棵樹不動,從第二棵二叉樹開始,依次把後一棵二叉樹的根節點作為前一棵二叉樹的根節點的右孩子,用線連起來。當所有二叉樹都連線起來之後就得到了森林轉化來的二叉樹。

 

 

   3,二叉樹轉化為樹

  把樹轉二叉樹的過程反過來做,步驟如下:

  (1)加線。如果某結點的左孩子存在,則將這個左孩子的右孩子結點、右孩子的右孩子結點、…… 。總之就是將左孩子的n個右孩子結點都作為此結點的孩子連線起來。

  (2)去線。刪除原二叉樹中所有結點與其右孩子的連線

  (3)層次調整,使之結構層次分明。

  4,二叉樹轉化為森林

  看一棵二叉樹能轉換成一棵樹還是森林,標準就是要看這棵二叉樹的根節點有沒有右孩子,有就是森林,沒有就是二叉樹。

  二叉樹轉森林的步驟如下:

  (1)從根節點開始,若右孩子存在,則把與右孩子的連線斷掉,知道所有的右孩子連線都刪除。

  (2)再將每棵分離出來的二叉樹轉換為樹即可

  5,樹與森林的遍歷

  (1)樹的遍歷方式分兩種: 第一種是先根遍歷樹。即先訪問樹的根結點,再依次訪問根的每棵子樹。

    第二種是後根遍歷,即先依次後根遍歷每棵子樹,再訪問根節點。例如上面圖中的樹,先根遍歷結果為ABEFCDG,後根遍歷結果為EFBCGDA

  (2)森林的遍歷也分為兩種: 

    前序遍歷:先訪問森林中第一棵樹的根結點,然後再依次先根遍歷根的每棵子樹,再依次用同樣的方式遍歷除去第一棵樹的剩餘樹構成的森林。如上面圖中的森林結果為ABCDEFGHJI

    後序遍歷:先訪問森林中的第一棵樹,後根遍歷的方式遍歷每棵樹,然後再訪問根節點,再依次用同樣的方式遍歷去除第一棵樹的剩餘樹構成的森林,結果為BCDAFEJHIG

    我們分析發現,森林的前序遍歷和二叉樹的前序遍歷結果相同,森林的後序遍歷和二叉樹的中序遍歷結果相同。也就是說當以二叉連結串列作為樹的儲存結構時,樹的遍歷完全可以借用二叉樹的遍歷演算法來實現。

九、赫夫曼樹及其應用

  1,赫夫曼樹

  從樹中一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目稱作路徑長度。樹的路徑長度就是從樹根到每一結點的路徑長度之和。

  如果考慮帶權的結點,結點的帶權路徑長度就是從該結點到樹根之間的路徑長度與結點上權的乘積。帶權路徑長度WPL最小的二叉樹稱作赫夫曼樹。

  例如,給定

分數 0~59 60~69 70~79 80~89 90~100
所佔比例% 5 15 40 30 10

步驟:(1)把結點按權值從小到大排成一個有序序列 A5,E10,B15,D30,C40

   (2)取前兩個最小權值的結點作為新結點N1的兩個子結點,小的為左孩子

 

 

   (3)將N1替換A與E插入到序列中,即N1 15,B15,D30,C40,重複(2)

 

 

   (4)反覆重複(2),(3)步驟,完成建立

 

 

 通過上面的步驟,我們可以得出構造赫夫曼樹的赫夫曼演算法描述:

  1,根據給定的n個權值構成n棵二叉樹的集合F={T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權為wi的根節點,其左右子樹為空。

  2,在F中選取兩棵根節點的權值最小的樹作為左右子樹構造一棵新二叉樹,並且置新二叉樹的根節點的權值為左右子樹上根節點的權值之和。

  3,在F中刪除這兩棵樹,同時將新得到的二叉樹加入F

  4,重複2,3直到F只含一棵樹為止。這棵樹便是赫夫曼樹。

程式碼如下:

  

//結點結構

import java.util.Comparator;

public class TNode<T> implements Comparable{
    private T data;
    private int weight;
    private TNode lChild;
    private TNode rChild;

    private Comparator comparator;

    public TNode(T data, int weight){
        this.data = data;
        this.weight = weight;
        this.lChild = null;
        this.rChild = null;
    }

    public TNode(T data, int weight, TNode lChild, TNode rChild){
        this(data, weight);
        this.lChild = lChild;
        this.rChild = rChild;
    }

    public Comparator getComparator(){
        return this.comparator;
    }
    public void setlChild(TNode node){
        this.lChild = node;
    }

    public void setrChild(TNode node){
        this.rChild = node;
    }

    public int getWeight(){
        return this.weight;
    }

    public T getData(){
        return data;
    }

    public TNode getlChild(){
        return lChild;
    }

    public TNode getrChild(){
        return rChild;
    }

    @Override
    public int compareTo(Object o) {
        return ((TNode)o).weight - this.weight;
    }

}

class TNodeComparator implements Comparator{

        @Override
        public int compare(Object o1, Object o2) {
            return ((TNode)o1).getWeight() - ((TNode)o2).getWeight();
        }

}
//建立霍夫曼樹

import java.util.LinkedList;

public class HuffmanTree<T> {
    LinkedList<TNode<T>> tnodes = new LinkedList<>();

    public HuffmanTree(LinkedList<TNode<T>> tnodes){
        this.tnodes = tnodes;
    }

    public TNode<T> buildHuffmanTree(){
        tnodes.sort(new TNodeComparator());
        while (tnodes.size() > 1) {
            TNode<T> newTNode = buildBiTree(tnodes.remove(0), tnodes.remove(0));
            tnodes.add(0, newTNode);
            tnodes.sort(new TNodeComparator());
        }
        return tnodes.remove(0);
    }

    public void PreOrderTraverse(TNode tNode){
        if (tNode == null){
            return;
        }else {
            T tdata = (T) tNode.getData();
            if (tdata != null){
                System.out.println(tdata);
            }
            PreOrderTraverse(tNode.getlChild());
            PreOrderTraverse(tNode.getrChild());
        }
    }

    private TNode buildBiTree(TNode<T> tNode, TNode<T> tNode1) {
        TNode<T> t1 = new TNode<T>(null, tNode.getWeight() + tNode1.getWeight(), tNode, tNode1);
        return t1;
    }
}

 2,赫夫曼樹的應用 —— 赫夫曼編碼

  當年赫夫曼研究赫夫曼樹,就是為了解決遠距離通訊中資料傳輸的最優化問題。比如我們有一段文字內容為“BADCADFEED”要傳送給別人,用二進位制是顯然的方法。我們這段文字中包含ABCDEF六種字母,我們就可以相應的編碼為

A B C D E F
000 001 010 011 100 101

 

傳輸時就按照上面編碼的對應二進位制來傳輸,解碼時也按照三位一分隔來解碼。假設我們有很長的一段字元,六個字母的頻率為A 27, B 8, C 15, D 15, E 30 , F 5,合起來正好是100%,我們就可以用赫夫曼樹來規劃它們。

 

 我們對左圖的赫夫曼樹的權值左分支改為0,右分支改為1,這樣就可以將六個葉子結點按照路徑重新編碼

A B C D E F
01 1001 101 00 11 1000

這樣壓縮了許多傳輸成本。當我們接收到傳輸過來的新編碼時,要按照約定好的赫夫曼樹來解碼。因為長度不均,所以必須任意字元的編碼都不是另一個字元編碼的字首,這叫做字首編碼,而赫夫曼樹的編碼剛好滿足這種條件。

赫夫曼樹時字首編碼,且是最優字首編碼。下面是嚴蔚敏老師資料結構中的證明

 

總結:

  本章重點是二叉樹部分的前序、中序、後序以及層序遍歷,程式碼要會寫,遞迴方法程式碼優雅但容易棧溢位,非遞迴方法不易棧溢位但程式碼要比較難理解。線索二叉樹的構造和遍歷也要理解。

&n