【圖解數據結構】樹及樹的遍歷


當你第一次學習編碼時,大部分人都是將數組作為主要數據結構來學習。

之后,你將會學習到哈希表。如果你是計算機專業的,你肯定需要選修一門數據結構的課程。上課時,你又會學習到鏈表,隊列和棧等數據結構。這些都被統稱為線性的數據結構,因為它們在邏輯上都有起點和終點。

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

定義

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

樹的定義

樹(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

結合下圖很好理解:

 

 

二叉樹的存儲結構

二叉樹順序存儲結構

一般二叉樹:

^ 代表不存在的結點。

 

二叉鏈表

鏈表每個結點包含一個數據域和兩個指針域:

mark

其中data是數據域,lchild和rchild都是指針域,分別指向左孩子和右孩子。

mark

 

二叉樹的遍歷

深度優先搜索(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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM