【圖解資料結構】樹及樹的遍歷
當你第一次學習編碼時,大部分人都是將陣列作為主要資料結構來學習。
之後,你將會學習到雜湊表。如果你是計算機專業的,你肯定需要選修一門資料結構的課程。上課時,你又會學習到連結串列,佇列和棧等資料結構。這些都被統稱為線性的資料結構,因為它們在邏輯上都有起點和終點。
當你開始學習樹和圖的資料結構時,你會覺得它是如此的混亂。因為它的儲存方式不是線性的,它們都有自己特定的方式儲存資料。
定義
樹是眾所周知的非線性資料結構。它們不以線性方式儲存資料。他們按層次組織資料。
樹的定義
樹(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) 有:
如果i=1,則結點i是二叉樹的根,無雙親;如果 i>1,則其雙親是結點 [ i / 2 ]。 雙親結點的編號 = 兩個子結點中的一個子結點 / 2
- 如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子是結點2i
如果2i+1>n,則結點 i 無右孩子;否則其右孩子是結點2i+1
結合下圖很好理解:
二叉樹的儲存結構
二叉樹順序儲存結構
一般二叉樹:
^ 代表不存在的結點。
二叉連結串列
連結串列每個結點包含一個數據域和兩個指標域:
其中data是資料域,lchild和rchild都是指標域,分別指向左孩子和右孩子。
二叉樹的遍歷
深度優先搜尋(Depth-First Search,DFS)
DFS 在回溯和搜尋其他路徑之前找到一條到葉節點的路徑。讓我們看看這種型別的遍歷的示例。
輸出結果為: 1–2–3–4–5–6–7
為什麼?
讓我們分解一下:
-
從根結點(1)開始。輸出
-
進入左結點(2)。輸出
-
然後進入左孩子(3)。輸出
-
回溯,並進入右孩子(4)。輸出
-
回溯到根結點,然後進入其右孩子(5)。輸出
-
進入左孩子(6)。輸出
-
回溯,並進入右孩子(7)。輸出
-
完成
當我們深入到葉結點時回溯,這就被稱為 DFS 演算法。
既然我們對這種遍歷演算法已經熟悉了,我們將討論下 DFS 的型別:前序、中序和後序。
前序遍歷
這和我們在上述示例中的作法基本類似。
-
輸出節點的值
-
進入其左結點並輸出。當且僅當它擁有左結點。
-
進入右結點並輸出之。當且僅當它擁有右結點
程式碼實現 -- 迭代實現
/** * 前序遍歷--迭代 */ 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