數據結構與算法 —— 05 樹

1.樹(Tree):

樹是 n(n>=0) 個結點的有限集。
當 n=0 時稱為空樹。在任意一顆非空樹中:有且僅有一個特定的稱為根(Root)的結點
當 n>1 時,其余結點可以分為 m(m>0) 個互不相交的有限集 T1,T2,..,Tm, 其中每一個集合本身又是一顆樹,并且稱為根的子樹(SubTree)

樹的分類結構圖


            ┌ 普通樹             ┌ 斜樹(左斜樹、右斜樹)
            │                     │ 
      ┌ 樹1 ┤ 二叉樹(BinaryTree)  ┤ 滿二叉樹:樹深 log(n+1)
      │     │                     │
      │     │                     └ 完全二叉樹(重要): 樹深 [logn]+1         
      │     └ ...
      │
森林 ─┤ 樹2
      │ ...
      │
      └

本質:是一對多的數據結構

(1)結點分類

結點的度:結點擁有的子樹的數稱為該結點的度(Degree)。
葉結點:度為零的結點稱為葉結點
分支結點(非終端結點,內部結點): 度不為 0 的結點
樹的度:指該樹中各結點的度的最大值稱為該樹的度。

(2)結點的關系

孩子:結點的子樹的根稱為該結點的孩子(Child)
雙親:該結點稱為其孩子的雙親(Parent)

(3)結點的層次(這個層次的概念是針對結點而言的)

從根開始定義起,根為第一層,根的孩子為第二層。

(4)樹的深度(高度)

樹中結點的最大層次稱為樹的深度(Deep),很顯然,該結點肯定是樹的根結點了。

注意樹的深度樹的度是不同的概念

(5)有序樹和無序樹

樹中個結點的子樹從左至右(或從右至左)是有序的,不能互換,則稱該樹為有序樹,否則為無序樹

(6)森林

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

2.樹的存儲結構

這里已經不能單純的采用前面的順序存儲、鏈式存儲,介紹三種存儲方式:雙親表示法、孩子表示法、孩子兄弟表示法

(1)雙親表示法(以父結點的角度)

用一組連續的空間存儲樹的結點,同時在每一個結點中,附設一個指示器指示雙親結點在數組中的位置。

┌────────┬────────┐
│ data   │ parent │
└────────┴────────┘

'結點描述'

public class PNode<T> { 
    public int parent; //父結點的位置
    public T data; //數據域
}
(2)孩子表示法(以孩子結點的角度)

將每個結點的孩子結點排列起來,以單鏈表的形式作為存儲結構,則n個結點有n個孩子鏈表,如果是葉子結點則此單鏈表為空,然后n個頭指針又組成一個線性表,采用順序存儲結構,存放入一個一維數組中。

1  A -> 1 -> 2
2  B -> 3
3  C -> 4 -> 5
4  D -> 6 -> 7 -> 8
5  E -> 9
6  F

為此要使用兩種結點結構:
(1)表頭結點

┌────┬──────────┐
│data│firstchild│
└────┴──────────┘

(2)孩子結點

┌─────┬────┐
│child│next│
└─────┴────┘

優點:方便查找某個結點的兄弟,只需要遍歷相關結點的孩子鏈表即可。遍歷整顆樹也是很方便,循環輸出整個頭結點數組即可

#######(3)孩子兄弟表示法(以結點的兄弟為角度)
優點是將一顆復雜的樹變成了一顆二叉樹

┌────┬──────────┬──────────┐
│data│firstchild│rightchild│
└────┴──────────┴──────────┘

'代碼描述'
(1)樹的結點描述

public class TreeNode<T> {
    public TreeNode<T> lChild; //左孩子
    public TreeNode<T> rChild; //右孩子
    private T data; //數據域
    //結點初始化
    public TreeNode() {
        data = null;
        lChild = null;
        rChild = null;          
    }
    public TreeNode(T x) {
        data = x;
        lChild = null;
        rChild = null;          
    }       
}

'樹的數據類型定義'

public class Tree {
    //其他的一些操作
    ...
}
3.二叉樹(Binary Tree)

是一種特殊的樹。(普通樹是可以和二叉樹相互轉換)
定義:是 n(n>=0) 個結點的有限集合,該集合或者為空集(稱為空二叉樹),或者有一個根結點和兩棵互不相交的、分別為根結點的左子樹和右子樹的二叉樹組成。

(1)二叉樹的特點

1)由于每個結點最多有兩棵子樹,所以二叉樹中不存在度大于2的結點;側面也證明了,二叉樹的度 <= 2
2)左子樹和右子樹是有順序的,不可以顛倒
3)即使二叉樹中某個結點只有一棵子樹,也要區分是左子樹還是右子樹。

(2)特殊的二叉樹

斜樹:所有結點都只有左子樹或都只有右子樹。分別稱為左斜樹、右斜樹
滿二叉樹(有2個條件):所有的分支結點都有左子樹和右子樹,且所有的葉子結點在同一層上。

**滿二叉樹的特點: **

  1. 葉子結點只能出現在最下層
  2. 非葉子結點的度均為 2
  3. 在同樣深度的二叉樹中,滿二叉樹的結點個數最多,葉子結點最多

③ 完全二叉樹:如果編號為 i(1<= i <= n) 的結點與同樣深度的滿二叉樹中編號為 i 的結點在二叉樹中的位置完全相同,則稱為完全二叉樹。

完全二叉樹的特點:
1)樹的編號是連續
2)葉子結點只能出現在最下兩層
3)最下層的葉子結點一定集中在左部連續的位置

(3)二叉樹的性質(理解記憶)

1)在二叉樹的第 i 層上至多有 2^(i-1)個結點
2)深度為k的二叉樹至多有 2^k-1 個結點(k >= 1)
注:當為滿二叉樹的時候,結點個數為:2^k-1

3)對任何一顆二叉樹T, 如果其終端結點(即葉子結點)數為n0,度為2的結點數為 n2,則有 n0=n2+1;
4)具有 n 個結點的滿二叉樹的深度為: ** log(n+1)**
5)具有 n 個結點的完全二叉樹的深度為: ** [logn]+1**

注意:這是針對于完全二叉樹,不是滿二叉樹
推導過程:

由于深度為 k 的滿二叉樹的結點個數:n = 2^k-1
所以,深度為 k 的完全二叉樹的結點個數:n, 滿足:
      2^(k-1)-1 < n <= 2^k-1
由于n是整數,因此
——> 2^(k-1) - 1 < n < 2^k
   ——> 2^(k-1) <= n < 2^k
        —— k-1 < logn <= k
由于k取整數  ——> k = [logn]+1 

6)如果對一棵有 n 個結點的完全二叉樹(易知其深度[logn]+1),將其結點按層編號(從第1層到[logn]+1)層,從左到右),對任意結點 i (i<= i <= n)有:
ⅰ) i = 1, 則i為根結點,無雙親,如果i>1, 則其雙親結點編號:[i/2]
ⅱ) 如果 2i>n, 則結點 i 無左孩子(且i為葉子結點);否則其左孩子編號:2i
ⅲ) 如果 2i+1>n, 則無右孩子;否則右孩子的編號:2i+1

(4)二叉樹的存儲

存儲方式和普通樹的存儲方式還是有很大的差別,尤其是它可以實現順序存儲

1)順序存儲結構

用一維數組存儲二叉樹中的結點,并且結點的存儲位置是可以反映出各個結點之間的邏輯關系。(這點是不同于普通樹)

對于"完全二叉樹"而言:

┌───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ F │ G │
└───┴───┴───┴───┴───┴───┴───┘

對于"普通的二叉樹"可以當成完全二叉樹來存儲,只是把沒有結點的地方設為"^"

┌───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ ^ │ E │ ^ │ G │
└───┴───┴───┴───┴───┴───┴───┘

對于"斜二叉樹"而言就有些浪費了空間,因此,這種順序存儲結構比較適合"完全二叉樹"

2)鏈式存儲結構

由于二叉樹的每個結點最多有2個孩子,因此,其結點可以設計成如下形式:

┌────────┬─────┬────────┐
│ lChild │ data│ rChild │
└────────┴─────┴────────┘

'代碼描述'

public class BinTreNode<T> {
    T data; //數據域
    BinTreNode<T> lChild; //左孩子結點指針
    BinTreNode<T> rChild; //右孩子節點指針
    public BinTreNode() {
        this.data = null;
        this.lChild = null;
        this.rChild = null;                 
    }
    public BinTreNode(T x) {
        this.data = x;
        this.lChild = null;
        this.rChild = null;                 
    }
}

注意:如果為了方便找某個結點的雙親結點,就同普通樹中處理方式一樣,增加一個指向其雙親結點 parent 的指針域即可:

┌──────┬────┬──────┬──────┐
│lChild│data│rChild│parent│
└──────┴────┴──────┴──────┘
(5)遍歷二叉樹(Traversing binary Tree)

含義:是指從根結點出發,按照某種"次序"依次"訪問"二叉樹的所有結點并且只被訪問一次。

二叉樹的遍歷不同于線性數據結構:
因為線性數據結構中結點都是有唯一的前驅或后繼結點,這使得遍歷結果是唯一確定;
然而,在二叉樹(普通樹也是如此)中每個結點的后繼結點不唯一,可以有多種選擇,因此選擇
不同,遍歷結果也就不同了。

二叉樹:

        A
      ╱ ╲
     B      C
   ╱    ╱  ╲
  D     E      F
╱  ╲   ╲
G      H    I
二叉樹常見的遍歷方式:

1)前序遍歷: ABDGHCEIF
規則:若二叉樹為空,則遍歷結果返回空。否則先訪問根結點、左子樹、右子樹

2)中序遍歷: GDHBAEICF
規則:若二叉樹為空,則遍歷結果返回空。否則先從根結點開始(不是訪問根結點),中序遍歷根結點的左子樹、根結點、右子樹

3)后序遍歷: GHDBIEFCA
規則:若二叉樹為空,則遍歷結果返回空。否則先從根結點開始(不是訪問根結點),后序遍歷根結點的左子樹、右子樹、根結點。

4)層序遍歷(層次遍歷):ABCDEFGHI
規則:若二叉樹為空,則遍歷結果返回空。否則從樹的根結點開始遍歷,從上至下,逐層遍歷訪問

**注意: **
1.前、中、后遍歷方式,是針對"根結點"來說的
2.為什么要研究遍歷?
因為計算機只會處理線性序列,因此,我們需要研究如何把樹這種非線性序列轉變為線性序列。
3.已知前序遍歷序列和中序遍歷序列,可以唯一的確定一顆二叉樹
已知后序遍歷序列和中序遍歷序列,可以唯一的確定一顆二叉樹
但是,已知前序和后序遍歷序列,無法唯一的確定一顆二叉樹

'遍歷代碼描述':采用遞歸的方式很容易的完成

/**
 * 二叉樹的遍歷方式
 */
//前序遍歷
public void preOrder(BinaTreNode<T> node) {
    if (node == null) {
        return ;
    }
    //打印根結點
    System.out.print(node.data);
    preOrder(node.lChild);
    preOrder(node.rChild);      
}

//中序遍歷
public void inOrder(BinaTreNode<T> node) {
    if (node == null) {
        return ;
    }
    inOrder(node.lChild);
    //打印根結點
    System.out.print(node.data);
    inOrder(node.rChild);       
}

//后序遍歷
public void postOrder(BinaTreNode<T> node) {
    if (node == null) {
        return ;
    }
    postOrder(node.lChild);
    postOrder(node.rChild);
    //打印根結點
    System.out.print(node.data);
}

//層序遍歷:使用隊列來實現層序遍歷
public void levelOrder() {
    BinaTreNode<T>[] queue = new BinaTreNode[this.maxNodes];
    int front = -1; //隊首指針
    int rear = 0; //隊尾指針        
    
    if (this.root == null) {
        return;
    }
    queue[rear] = this.root;//二叉樹的根結點進隊
    //若隊不為空,則繼續遍歷
    while(rear != front) {
        front ++;
        //打印根結點
        System.out.print(queue[front].data);
        //將隊首結點的左孩子進隊
        if (queue[front].lChild != null) {
            rear ++;
            queue[rear] = queue[front].lChild;
        }
        //將隊首的右孩子也進隊
        if (queue[front].rChild != null) {
            rear ++;
            queue[rear] = queue[front].rChild;
        }
    }           
}
4.線索二叉樹(Thread BinaryTree)

要是能知道二叉樹每一個結點的直接前驅結點或后驅結點是誰,將會為二叉樹的其他操作帶來方便。但是,二叉樹在存儲結點的時候,并沒有反映出來每一個結點的直接前驅結點或后驅結點是誰。只能在二叉樹的某種遍歷過程中動態的得到這些信息。

一個具有 n 個結點的二叉樹,對于其二叉鏈表存儲,一共有 2n 個指針域(每個結點有左右兩個孩子指針域),n-1 個分支線(即兩個結點之間連接線),因此,還有 2n-(n-1)=n+1 個指針域是空的,白白的浪費,沒有利用。因此,可以考慮使用這些空閑的指針域:
將某個結點空閑的左指針域(lChild) 用來存儲該結點在某種遍歷下的直接前驅結點
將某個結點空閑的右指針域(rChild) 用來存儲該結點在某種遍歷下的直接后繼結點

我們將這種指向前驅和后繼的指針稱為線索(Thread),加了線索的二叉樹稱為線索二叉樹

線索二叉樹的結點結構:

┌────────┬────────┬──────┬────────┬────────┐
│  ltag  │ lChild │ data │ rChild │ rtag   │
└────────┴────────┴──────┴────────┴────────┘

ltag, rtal 是兩個標志位(各只占了 1bit 空間),分別用來表 lChild 和 rChild 是表示左(右)孩子結點還是前驅(后繼)結點。

    ┌ 0, 表示左孩子指針

ltag = │
└ 1, 表示前驅結點指針

    ┌ 0, 表示右孩子指針

rtag = │
└ 1, 表示后繼結點指針

線索二叉樹因遍歷順序不同,獲得的線索二叉樹也不同:
前序線索二叉樹
中序線索二叉樹
后序線索二叉樹

'結點代碼描述'

/**
 * 線索二叉樹的結點
 * @author Administrator
 *
 */
public class ThreadedTreNode<T> {
    public T data; //數據域
    public ThreadedTreNode<T> lChild; //左指針域
    public ThreadedTreNode<T> rChild; //右指針域
    //左標志位, 這是為了后面代碼方便才寫成boolean類型的
    public boolean ltag; //true表示為前驅結點指針
    //右標志位, 這是為了后面代碼方便才寫成boolean類型的
    public boolean rtag; //true表示后繼結點指針
    
    public ThreadedTreNode() {
        data = null;
        lChild = null;
        rChild = null;
        ltag = false; //默認表示左右孩子
        rtag = false;       
    }
    
    public ThreadedTreNode(T x) {
        data = x;
        lChild = null;
        rChild = null;
        ltag = false;
        rtag = false;       
    }
}

'線索二叉樹代碼描述'

/**
 * 線索二叉樹
 * @author Administrator
 *
 */
public class ThreadedTree<T> {
    /**
    * 頭結點,只是為了方便操作而增設的.
    * 其結構與其他線索二叉樹的結點結構一樣,只是數據域不存放信息,其
    * 左指針指向二叉樹的根結點,右指針指向自己。
    * 而原二叉樹在某種遍歷下的第一個結點的前驅線索和最后一個結點的后繼線索
    * 都指向該頭結點
    */
    private ThreadedTreNode<T> head; //
    private ThreadedTreNode<T> pre; //表示剛剛訪問過的結點
    
    //創建一棵包含頭結點的線索二叉樹
    public ThreadedTree() {
        this.head = new ThreadedTreNode<T>();       
    }
    
    /**
     * 通過中序遍歷的序列對二叉樹進行線索化
     * @return
     */
    public boolean startInThreading() {
        if(head == null) {
            return false;           
        }
        //設置head結點為頭結點,其左子結點指向根結點
        head.ltag = false; 
        head.rtag = true;
        head.rChild = head; //頭結點的右指針指向自身。
        if(head.lChild == null) {
            //若二叉樹為空,則左指針指向自身
            head.lChild = head;
        } else {
            //pre始終指向剛剛訪問過的結點。
            pre = head; //設置默認的前驅結點
            inThreading(head); //按中序遍歷進行中序線索化
            //對最后一個結點線索化
            pre.rChild = head;
            pre.rtag = true;
        }
        
        return true;        
    }
    
    //中序完成二叉樹線索化
    private void inThreading(ThreadedTreNode<T> p) {
        //p表示指向當前結點
        if(p == null) {
            return;         
        }
        inThreading(p.lChild); //左子樹線索化
        
        if(p.lChild == null) {
            //表明當前結點的沒有左孩子(左指針域為空),因此,該結點是有前驅結點的。
            // 此時,其前驅結點 pre 剛剛被訪問過
            //線索化
            p.ltag = true; //表明左指針是前驅結點指針
            p.lChild = pre;         
        }
        
        // 由于此時p結點的后繼還沒有被訪問到,只能對他的前驅結點pre的右指針進行判斷
        if(pre.rChild == null) {
            //表明 p 是 pre 的后繼
            pre.rtag = true;
            pre.rChild = p;         
        }
        
        pre = p; //保持 pre 指向 p 的前驅      
        inThreading(p.rChild); //右子樹線索化     
    }
    
    //遍歷二叉線索樹
    public void traversing() {
        ThreadedTreNode<T> node = head.lChild;
        if(node == null) {
            return;         
        }
        while(!node.ltag) {
            //尋找中序序列的首結點
            node = node.lChild;
            do {
                if (node != null) {
                    System.out.println(node.data);
                    node = searchPostNode(node);
                }
            } while (node.rChild != head);
        }
    }
    /**
     * 尋找中序的后繼結點
     * @param node
     * @return
     */
    public ThreadedTreNode<T> searchPostNode(ThreadedTreNode<T> node) {
        ThreadedTreNode<T> q = node.rChild;
        if (!node.rtag) {
            while(!q.rtag) {
                q = q.lChild;               
            }
        }
        return q;       
    }
    
    /**
     * 尋找中序的前繼結點
     * @param node
     * @return
     */
    public ThreadedTreNode<T> searchPreNode(ThreadedTreNode<T> node) {
        ThreadedTreNode<T> q = node.lChild;
        if (!node.ltag) {
            while(!q.ltag) {
                q = q.rChild;               
            }
        }
        return q;       
    }
}
5.普通樹、森林、二叉樹之間的轉換

(1)轉換
1)樹 ——> 二叉樹
步驟:1) 在所有的兄弟之間加一條連線
2) 對樹中每一個結點,只保留它與第一個孩子的連線,刪除與其他孩子的連線
3) 層次調整。簡單的理解:想像用手捏住根結點往起來一提溜,靠重力下垂,
便可得到調整后的層次

2)森林 ——> 二叉樹
步驟:1) 把每個樹轉換為二叉樹
2) 第一個二叉樹不動,從第二棵開始,依次把后一棵二叉樹的根結點作為前一棵根結點的右孩子

3)二叉樹 ——> 樹
是上面樹到二叉樹的逆過程
4)二叉樹 ——> 森林
如果這棵二叉樹有右孩子,那么該二叉樹就能轉換為森林是上面森林到二叉樹的逆過程

(2)樹與森林的遍歷
1)樹的遍歷
先根遍歷 (類似先序遍歷)
后跟遍歷 (類似后跟遍歷)
2)森林遍歷
前序遍歷(先訪問第一棵樹,每棵樹內用先根遍歷)
后序遍歷(先訪問第一棵樹,每棵樹內用后跟遍歷)

注意:森林的前序遍歷和二叉樹的前序遍歷結果相同
森林的后序遍歷和二叉樹的中序遍歷結果相同

因此,當以二叉鏈表來存儲樹時,其先根遍歷和后根遍歷算法完全同二叉樹的前序遍歷和后序遍歷

這樣就可以將樹和森林這種復雜問題進行簡單處理

6.二叉樹的應用:Huffman樹與Huffman編碼

(1)幾個概念:
1)路徑長度:從樹中一個結點到另一個結點之間的分支(其實就是結點之間的連線)構成兩個結點之間的路徑,而把這條路徑上的的分支(即連線)數目(之和)稱做路徑長度。
注意:"路徑長度" 是針對任意兩個結點間而言的

2)樹的路徑長度:指從樹根到每一個結點的路徑長度之和(對就是字面意思_)
3)結點的帶權路徑長度:該結點到樹根結點之間的路徑長度與該結點上權值的乘積
4)樹的帶權路徑(WPL):樹中所有葉子結點的帶權路徑之和

WPL = ∑ W(k)*L(K)

其中,W(k)為葉子結點的權值,L(k)為葉子結點的路徑長度

5)Huffman樹:把WPL最小的二叉樹稱為Huffman樹

(2)如何構造Huffman樹 ?

根據Huffman樹的定義知:要想使WPL最小,必須是權值越大的葉子結點越靠近根結點,而權值越小的葉子結點越遠離根結點。

基本思想如下:
ⅰ)把所有包含權值的數據元素(w1, w2, ..., wn)看成離散的葉子結點,并組成"結點集合": F={w1, w2, ..., wn}

ⅱ)從集合中選取權值最小的和次小的兩個葉子結點作為左右子樹構造成一棵新的二叉樹,則該二叉樹的根結點(記為,R(i),i表示第i個合成的根結點 )的權值為其左右子樹根結點的權值之和

ⅲ)從結點集合中剔除剛選取過的作為左右子樹的那兩個葉子結點,并將新構建的二叉樹的根結點(為R(i) )加入到結點集合中。

ⅳ)重復(ⅱ)(ⅲ)兩步,當集合中只剩下一個結點時,該結點就是所建立的Huffman樹的根結點,該二叉樹便為Huffman樹

注意:對于一組給定的葉子結點所組成的Huffman樹,其樹形可能不相同,但其WPL一定是相等的,且為最小

(3)Huffman編碼

Huffman樹最早是用于優化電文編碼的。減小電文編碼長度,節約存儲或傳輸成本。

如:A B   C   D   E   F (字符,即葉子結點)
    27  8   15  15  30  5 (字符出現的頻率或權值)

構造Huffman樹
將Huffman樹的左分支代表0,右分支代表1
    則,相應的Huffman編碼:

    A     B      C      D   E     F
    01  1001    101     00  11  1000
    
                ○
            ╱     ╲
          ╱         ╲
        (42)           (58)
      ╱   ╲       ╱  ╲
    D(15)   A(27)  (28)   E(30)
                  ╱  ╲
                (13)  C(15)
               ╱  ╲
             F(5)  B(8)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 數據結構和算法--二叉樹的實現 幾種二叉樹 1、二叉樹 和普通的樹相比,二叉樹有如下特點: 每個結點最多只有兩棵子...
    sunhaiyu閱讀 6,519評論 0 14
  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個結點至多有m個孩子。 除根結點和葉子結點外,其它每個結點至少有m...
    文檔隨手記閱讀 13,336評論 0 25
  • 1.樹的定義 樹是n(n>=0)個結點的有限集.n=0時稱為空樹.在任意一顆非空樹種:(1)有且僅有一個特定的稱為...
    e40c669177be閱讀 2,893評論 1 14
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,821評論 0 19
  • 前面講到的順序表、棧和隊列都是一對一的線性結構,這節講一對多的線性結構——樹?!敢粚Χ唷咕褪侵敢粋€元素只能有一個前...
    Alent閱讀 2,272評論 1 28