1. 程式人生 > >【圖解資料結構】樹及樹的遍歷

【圖解資料結構】樹及樹的遍歷

當你第一次學習編碼時,大部分人都是將陣列作為主要資料結構來學習。

之後,你將會學習到雜湊表。如果你是計算機專業的,你肯定需要選修一門資料結構的課程。上課時,你又會學習到連結串列,佇列和棧等資料結構。這些都被統稱為線性的資料結構,因為它們在邏輯上都有起點和終點。

當你開始學習樹和圖的資料結構時,你會覺得它是如此的混亂。因為它的儲存方式不是線性的,它們都有自己特定的方式儲存資料。

定義

樹是眾所周知的非線性資料結構。它們不以線性方式儲存資料。他們按層次組織資料。

樹的定義

樹(Tree)是n(n>=0)個結點的有限集。n=0時稱為空樹。

在任意一顆非空樹中:

(1)有且僅有一個特定的稱為根(Root)的結點。

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

下圖就符合樹的定義:

其中根結點A有兩個子樹:

 

我們硬碟的檔案系統就是很經典的樹形結構。

“樹”它具有以下的特點:

    ①每個節點有零個或多個子節點;

    ②沒有父節點的節點稱為根節點;

    ③每一個非根節點有且只有一個父節點;

    ④除了根節點外,每個子節點可以分為多個不相交的子樹;

 

樹( tree)是被稱為結點( node)的實體的集合。結點通過邊( edge)連線。每個結點都包含值或資料( value/date),並且每結節點可能有也可能沒有子結點。

樹的首結點叫根結點(即 root結點)。如果這個根結點和其他結點所連線,那麼根結點是父結點與根結點連線的是子結點。

所有的結點都通過邊連線。它是樹中很重要得一個概念,因為它負責管理節點之間的關係。

葉子結點是樹末端,它們沒有子結點。像真正的大樹一樣,我們可以看到樹上有根、枝幹和樹葉。

術語彙總 

  • 根結點是樹最頂層結點

  • 邊是兩個結點之間的連線

  • 子結點是具有父結點的結點

  • 父結點是與子結點有連線的結點

  • 葉子結點是樹中沒有子結點的結點(樹得末端)

  • 高度是樹到葉子結點(樹得末端)的長度

  • 深度是結點到根結點的長度

 

樹的結點

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

結點擁有的子樹數稱為結點的度(Degree)。

樹的度是樹內各結點度的最大值。

 

 結點的層次從根開始定義起,根為第一層,根的孩子為第二層,以此類推,若某結點在第 i 層,則其子樹的根就在第 i+1 層。

其雙親在同一層的結點互為堂兄弟。顯然下圖中的D、E、F是堂兄弟,而G、H、l、J也是。

樹的深度(Depth)或高度是樹中結點的最大層次。 

 

樹的高度( height)和深度( depth)

  • 樹的高度是到葉子結點(樹末端)的長度,也就是根結點到葉子結點的最大邊長度

  • 結點的深度是它到根結點的長度,也就是層次

 

 

樹的儲存結構

 

 

 雙親表示法

在每個結點中,附設一個指示器指示其雙親結點到連結串列中的位置。

 

  

 

優點:parent指標域指向陣列下標,所以找雙親結點的時間複雜度為O(1),向上一直找到根節點也快
缺點:由上向下找就十分慢,若要找結點的孩子或者兄弟,要遍歷整個樹

 

孩子表示法

 

 

優點:找孩子比較容易

缺點:佔用了大量不必要的孩子域空指標。 若要找結點的父親,要遍歷整個樹。

 

改進一:為每個結點新增一個結點度域,方便控制指標域的個數

 

 缺點:維護困難,不易實現   

 

改進二:結合順序結構和鏈式結構

把所有結點先放在數組裡面,每個結點都會有自己的子結點,第一個孩子就用一個指標表示,每個孩子的next指標指向它的兄弟

 

 

孩子兄弟表示法

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

 

 

 

二叉樹

二叉樹的定義 

二叉樹(Binary Tree)是n(n>=0)個結點的有限集合,該集合或者為空集(空二叉樹),或者由一個根結點和兩棵互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成(子樹也為二叉樹)。

二叉樹的特點

  • 每個結點最多有兩棵子樹,所以二叉樹中不存在度大於2的結點。
  • 左子樹和右子樹是有順序的,次序不能任意顛倒。
  • 即使樹中某結點只有一棵子樹,也要區分它是左子樹還是右子樹。

二叉樹五種基本形態

  1、空二叉樹

  2、只有一個根結點

  3、根結點只有左子樹

  4、根結點只有右子樹

  5、根結點既有左子樹又有右子樹

幾種特殊的二叉樹

斜樹

左斜樹:  右斜樹:

 

 

滿二叉樹

 

滿二叉樹:

 

 

完全二叉樹

完全二叉樹:

 

二叉樹的性質

二叉樹性質1

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

二叉樹性質2

性質2:深度為k的二叉樹至多有2k-1個結點(k>=1)

N=2K-1    K是層次/高度(4)   N=15

二叉樹性質3

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

一棵二叉樹,除了終端結點(葉子結點),就是度為1或2的結點。假設n1度為1的結點數,則數T 的結點總數n=n0+n1+n2。我們再換個角度,看一下樹T的連線線數,由於根結點只有分支出去,沒有分支進入,所以連線線數為結點總數減去1。也就是n-1=n1+2n2,可推匯出n0+n1+n2-1 = n1+2n2,繼續推導可得n0 = n2+1。

二叉樹性質4

性質4:具有n個結點的完全二叉樹的深度為[log2n +1] ([X]表示不大於X的最大整數)。

2K=N+1     N是結點數(15)

K=log2n+1  <  log2n+1

由性質2可知,滿二叉樹的結點個數為2k-1,可以推匯出滿二叉樹的深度為k=log2(n + 1)。對於完全二叉樹,它的葉子結點只會出現在最下面的兩層,所以它的結點數一定少於等於同樣深度的滿二叉樹的結點數2k-1,但是一定多於2k-1 -1。因為n是整數,所以2k-1 <= n < 2k,不等式兩邊取對數得到:k-1 <= log2n <k。因為k作為深度也是整數,因此 k= [log2n ]+ 1。

二叉樹性質5

性質5:如果對一顆有n個結點的完全二叉樹(其深度為 [ log2n+1 ] )的結點按層序編號(從第1層到第 [log2n+1] 層,每層從左到右),對任一結點 i (1<=i<=n) 有:

  1. 如果i=1,則結點i是二叉樹的根,無雙親;如果 i>1,則其雙親是結點 [ i / 2 ]。    雙親結點的編號 = 兩個子結點中的一個子結點  / 2

  2. 如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子是結點2i
  3. 如果2i+1>n,則結點 i 無右孩子;否則其右孩子是結點2i+1

結合下圖很好理解:

 

 

二叉樹的儲存結構

二叉樹順序儲存結構

一般二叉樹:

^ 代表不存在的結點。

 

二叉連結串列

連結串列每個結點包含一個數據域和兩個指標域:

其中data是資料域,lchild和rchild都是指標域,分別指向左孩子和右孩子。

 

二叉樹的遍歷

深度優先搜尋(Depth-First Search,DFS)

DFS 在回溯和搜尋其他路徑之前找到一條到葉節點的路徑。讓我們看看這種型別的遍歷的示例。

輸出結果為: 1–2–3–4–5–6–7

為什麼?

讓我們分解一下:

  1. 從根結點(1)開始。輸出

  2. 進入左結點(2)。輸出

  3. 然後進入左孩子(3)。輸出

  4. 回溯,並進入右孩子(4)。輸出

  5. 回溯到根結點,然後進入其右孩子(5)。輸出

  6. 進入左孩子(6)。輸出

  7. 回溯,並進入右孩子(7)。輸出

  8. 完成

當我們深入到葉結點時回溯,這就被稱為 DFS 演算法。

既然我們對這種遍歷演算法已經熟悉了,我們將討論下 DFS 的型別:前序、中序和後序。

前序遍歷

這和我們在上述示例中的作法基本類似。

  1. 輸出節點的值

  2. 進入其左結點並輸出。當且僅當它擁有左結點。

  3. 進入右結點並輸出之。當且僅當它擁有右結點

 程式碼實現 -- 迭代實現

/**
 * 前序遍歷--迭代
 */
public void preOrder(TreeNode node) {
    if (node == null) {
        return;
    } else {
        System.out.println("preOrder data:" + node.getData());
        preOrder(node.leftChild);
        preOrder(node.rigthChild);
    }
}

 

前序遍歷 - -棧實現

/**
 * 前序遍歷--棧
 *
 * @param node
 */
public void nonRecOrder(TreeNode node) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.push(node);
    while (!stack.isEmpty()) {
        //出棧和進棧
        TreeNode n = stack.pop();//彈出根節點
        //壓入子結點
        System.out.println("nonRecOrder data: " + n.getData());
        //避免葉子結點為空,出現空指標異常
        if (n.rigthChild != null) {
            stack.push(n.rigthChild);
        }
        if (n.leftChild != null) {
            stack.push(n.leftChild);
        }
    }
}

 

 

中序遍歷

 

 

示例中此樹的中序演算法的結果是3–2–4–1–6–5–7。

左結點優先,之後是中間,最後是右結點。

 程式碼實現:

/**
 * 中序遍歷--迭代
 */
public void midOrder(TreeNode node) {
    if (node == null) {
        return;
    } else {
        midOrder(node.leftChild);
        System.out.println("midOrder data:" + node.getData());
        midOrder(node.rigthChild);
    }
}

 

 

後序遍歷

以此樹為例的後序演算法的結果為 3–4–2–6–7–5–1 。

左結點優先,之後是右結點,根結點的最後。

 程式碼實現:

/**
     * 後序遍歷--迭代
     */
    public void

    postOrder(TreeNode node) {
        if (node == null) {
            return;
        } else {
            postOrder(node.leftChild);
            postOrder(node.rigthChild);
            System.out.println("postOrder data:" + node.getData());
        }
    }

 

 

自創遍歷小技巧(附連結)

先根遍歷法(超級簡單小技巧)

 

 

三角形遍歷法

 

 

 結果: G D  I H B A E J C F

 

例子:

上圖二叉樹遍歷結果

    前序遍歷:ABCDEFGHK

    中序遍歷:BDCAEHGKF

    後序遍歷:DCBHKGFEA

&n