前面我們講了都是線性表結構,比如:陣列、鍊錶、棧、佇列等。今天我們終於可以講一講樹了,樹
是非線性結構
。
我們都知道,對於大量的輸入資料,鍊錶的線性訪問太慢,不宜使用。我們今天講的樹,其大部分操作的執行時間平均為 o(logn
\log n
logn
)。講二叉樹之前我們先來思考一下這幾個問題。二叉樹有哪幾種儲存方式?什麼樣的二叉樹適合用陣列來儲存?帶著問題與思考,看完以後頓時會對二叉樹的設計原理豁然開朗的感覺。
好了,我們接下來開始二叉樹的學習之旅吧。要想學二叉樹,我們要先來了解樹與樹的一些特性。
樹(tree)
可以用幾種方式定義。定義樹的一種自然的方式是遞迴的公式。一棵樹是一些節點的集合。這個集合可以是空集;若不是空集,則樹由稱作根(root)
的節點r
以及 0 個或多個非空的(子)樹t1,t2,...,tk
組成,這些子樹中每一棵的根都被來自根r
的一條有向的邊(edge)
所鏈結。
上面關於樹的定義你應該清楚了吧,什麼?有點抽象?可能你剛開始接觸樹這種資料結構吧應該,沒有關係。***給張圖你就全部清楚了。
比如上圖,b 節點就是 e 節點的父節點
,e 節點是 b 節點的子節點
。b、c、d 這三個節點的父節點是同乙個節點,所以它們之間互稱為兄弟節點
。們把沒有父節點的節點叫作根節點
,也就是圖中的節點 a。我們把沒有子節點的節點叫作葉子節點
或者葉節點
,比如圖中的 e、i、j、g、h 都是葉子節點。
除此之外,關於樹,還有三個比較相似的概念:高度(height)
、深度(depth)
、層(level)
。它們的定義是這樣的:
這三個概念的定義比較容易混淆,描述起來也比較空洞。我舉個例子說明一下,你一看應該就能明白。
有乙個更好記的方法:在我們的生活中,「高度」這個概念,其實就是從下往上度量,比如我們要度量第 3 層樓的高度、第 21 層樓的高度,起點都是地面。所以,樹這種資料結構的高度也是一樣,從最底層開始計數,並且計數的起點是 0。
「深度」這個概念在生活中是從上往下度量的,比如水中魚的深度,是從水平面開始度量的。所以,樹這種資料結構的深度也是類似的,從根結點開始度量,並且計數起點也是 0。
「層數」跟深度的計算類似,不過,計數起點是 1,也就是說根節點的位於第 1 層。
樹結構多種多樣,不過我們最常用還是二叉樹。
二叉樹,顧名思義,每個節點最多有兩個「叉」,也就是兩個子節點,分別是左子節點和右子節點。不過,二叉樹並不要求每個節點都有兩個子節點,有的節點只有左子節點,有的節點只有右子節點。***畫的都是二叉樹。
這個圖裡面,有兩個比較特殊的二叉樹,分別是編號 2 和編號 3 這兩個。
其中,編號 2 的二叉樹中,葉子節點全都在最底層,除了葉子節點之外,每個節點都有左右兩個子節點,這種二叉樹就叫作滿二叉樹。
編號 3 的二叉樹中,葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他層的節點個數都要達到最大,這種二叉樹叫作完全二叉樹。
滿二叉樹很好理解,也很好識別,但是完全二叉樹,有的人可能就分不清了。我畫了幾個完全二叉樹和非完全二叉樹的例子,你可以對比著看看。
上圖中的編號 1 是完全二叉樹, 編號 2 和編號 3 這兩個不是完全二叉樹。這時你會滿臉疑惑的問,這三個感覺沒啥區別呀。為什麼編號 1 把最後一層的葉子節點靠左排列了就叫完全二叉樹了?如果靠右排列就不能叫完全二叉樹了嗎?
要理解完全二叉樹定義的由來,我們需要先了解,如何表示(或者儲存)一棵二叉樹?
想要儲存一棵二叉樹,我們有兩種方法,一種是基於指標或者引用
的二叉鏈式儲存法
,一種是基於陣列
的順序儲存法
。
我們先來看相對簡單的鏈式儲存法,從下圖你看到,每個節點有三個字段,其中乙個儲存資料,另外兩個是指向左右子節點的指標。你閉著眼睛把根節點拎起來,就可以通過左右子節點的指標,把整棵樹都串起來。這種儲存方式我們比較常用。大部分二叉樹**都是通過這種結構來實現的。
我們再來看,基於陣列的順序儲存法。我們把根節點儲存在下標 i = 1 的位置,那左子節點儲存在下標 2 * i = 2 的位置,右子節點儲存在 2 * i + 1 = 3 的位置。以此類推,b 節點的左子節點儲存在 2 * i = 2 * 2 = 4 的位置,右子節點儲存在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。
我來總結一下,如果節點 x 儲存在陣列中下標為 i 的位置,下標為 2 * i 的位置儲存的就是左子節點,下標為 2 * i + 1 的位置儲存的就是右子節點。反過來,下標為 i/2 的位置儲存就是它的父節點。通過這種方式,我們只要知道根節點儲存的位置(一般情況下,為了方便計算子節點,根節點會儲存在下標為 1 的位置),這樣就可以通過下標計算,把整棵樹都串起來。
不過,我剛剛舉的例子是一棵完全二叉樹,所以僅僅「浪費」了乙個下標為 0 的儲存位置。如果是非完全二叉樹,其實會浪費比較多的陣列儲存空間。你可以看我舉的下面這個例子。
所以,如果某棵二叉樹是一棵完全二叉樹,那用陣列儲存無疑是最節省記憶體的一種方式。因為陣列的儲存方式並不需要像鏈式儲存法那樣,要儲存額外的左右子節點的指標。這也是為什麼完全二叉樹要求最後一層的子節點都靠左的原因。
前面講了二叉樹的定義與儲存,我們再來看下二叉樹最重要的特性,二叉樹的遍歷。
如何將所有節點都遍歷列印出來呢?經典的方法有三種,前序遍歷、中序遍歷和後序遍歷。其中,前、中、後序,表示的是節點與它的左右子樹節點遍歷列印的先後順序。
實際上,二叉樹的前、中、後序遍歷就是乙個遞迴的過程。比如,前序遍歷,其實就是先列印根節點,然後再遞迴地列印左子樹,最後遞迴地列印右子樹。
前序遍歷的遞推公式:
preorder(r) = print r->preorder(r->left)->preorder(r->right)
中序遍歷的遞推公式:
inorder(r) = inorder(r->left)->print r->inorder(r->right)
後序遍歷的遞推公式:
postorder(r) = postorder(r->left)->postorder(r->right)->print r
從我前面畫的前、中、後序遍歷的順序圖,可以看出來,每個節點最多會被訪問兩次,所以遍歷操作的時間複雜度,跟節點的個數 n 成正比,也就是說二叉樹遍歷的時間複雜度是o(n)
。
1、表示式樹
如上圖所示顯示的乙個表示式樹(expression tree)
,表示式樹的樹葉是運算元,如常熟或變數名,而其它節點為操作符。
這個例子的中序遍歷的話,表示式樹表示的是:(a + b * c)+ ((d * e + f) * g)
換個前序或者後序遍歷的話,表示式又會不同,是不是很有意思。
小型計算的話可以用棧來實現,大型的計算我覺得可以用表示式樹來實現。
2、我們講了三種二叉樹的遍歷方式,前、中、後序。實際上,還有另外一種遍歷方式,也就是按層遍歷,你知道如何實現嗎?
層序遍歷。
可以參考leetcode-102. 二叉樹的層序遍歷
/**
* definition for a binary tree node.
* public class treenode
* }*/class
solution
private
void
levelorderhelper
(treenode node,
int level)
resultlist.
get(level)
.add
(node.val)
;levelorderhelper
(node.left, level +1)
;levelorderhelper
(node.right, level +1);}}
資料結構與演算法 (九)二叉樹
前序遍歷 中序遍歷 後序遍歷 遞迴實現 二叉搜尋樹 查詢樹 插入 刪除 查詢 平衡二叉搜尋樹 紅黑樹 遞迴樹 分析時間複雜度 leetcode 1 二叉樹的最近公共祖先 2 驗證二叉搜尋樹 思路 二叉搜尋樹 除了左子節點 節點 右子節點 節點 這個規則外,還有乙個規則是所有的左子樹節點,都應該小於該...
資料結構與演算法 樹結構 二叉樹 筆記整理《十四》
優點 通過下標查詢查詢速度快 對有序陣列可以通過二分查詢提高速度 缺點 如果檢索某個具體的值或者插入元素,會整體移動效率低 優點 插入效率高 缺點 檢索時需要遍歷所有節點查詢 既保持了類似鍊錶結構的插入效率,又保持了查詢的效率,查詢的特點很類似於陣列的二分查詢,比如二叉樹 每個節點都一分為二,父節點...
資料結構與演算法 樹與二叉樹
樹是若干個結點組成的有限集合,其中必須有乙個結點是根結點,其餘結點劃分為若干個互不相交的集合,每乙個集合還是一棵樹,稱為根的子樹。當樹的結點個數為0時,我們稱這棵樹為空樹,記為 關於樹的基本術語 結點 表示樹中的元素,包括資料項和若干指向其子樹的分支 結點的度 結點所擁有的子樹的個數 葉子結點 度為...