樹的定義
樹是一種抽象資料型別,用來模擬具有樹狀結構性質的資料集合。樹的專業術語比較多,需要了解一下:
- 樹的結點:包含一個資料元素及若干指向子樹分支的資訊
- 結點的度:一個結點含有的子樹的數目稱為該結點的度
- 樹的度:樹中最大的結點度稱為樹的度
- 葉子結點:也稱終端結點,結點度為零的結點
- 分支結點:也稱非終端結點,結點度不為零的結點
- 子結點:一個結點含有的子樹的根結點稱為該結點的子結點
- 父結點:若一個結點含有子結點,則這個結點稱為其子結點的父結點
- 兄弟結點:具有相同父結點的結點互稱為兄弟結點
- 堂兄弟結點:父結點在同一層的結點互為堂兄弟結點
- 結點的祖先:從根到該結點所經分支上的所有結點
- 子孫:以某結點為根的子樹中任一結點都稱為該結點的子孫
- 結點的層次:從根開始定義起,根為第 1 層,根的子結點為第 2 層,以此類推
- 深度:對於任意結點 n,n 的深度為從根到 n 的唯一路徑長,根的深度為 0
- 高度:對於任意結點 n,n 的高度為從 n 到葉子結點的最長路徑長,所有葉子結點的高度為 0
- 森林:由 m(m>=0) 棵互不相交的樹組成的集合稱為森林
二叉樹
樹的結構多種多樣,不過最常用的還是二叉樹。
顧名思義,二叉是指每個結點最多隻有兩個子結點,分別稱為左子結點和右子結點。但是,二叉樹並不要求所有結點必須擁有兩個子結點,有的結點只有左子結點,有的結點只有右子結點。
滿二叉樹
如圖 a 所示,除葉子結點以外,其餘的結點每個都有 2 個子結點,這種二叉樹被稱為滿二叉樹。
完全二叉樹
如圖 b 所示,除最後一層外,每一層的結點數均達到最大值,而且最後一層的葉子結點都靠左排列,只缺少右邊的若干結點,這種二叉樹被稱為完全二叉樹。
可以看得出,滿二叉樹是一種特殊的完全二叉樹。
二叉查詢樹
二叉查詢樹是一種特殊的二叉樹,常用作搜尋使用,也被稱為二叉搜尋樹、二叉排序樹。
它有可能是一棵空樹,也可能是具有以下性質的二叉樹:
- 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值
- 若它的右子樹不空,則右子樹上所有結點的值均大於等於它的根結點的值
- 它的左、右子樹也分別為二叉查詢樹
二叉查詢樹是一種經典的資料結構,它既具有連結串列快速插入、刪除的特點,又具有陣列快速查詢的優勢。
儲存結構
鏈式儲存
使用連結串列儲存樹的結構是一種比較簡單、直觀的方法。
二叉樹中每個結點最多隻有兩個子結點,因此,可以給結點設計一個資料域和兩個指標域,這兩個指標域分別指向左子結點和右子結點。
這種情況下,使用連結串列作為儲存方式,只要拎住根結點,就可以通過左右子結點的指標,把整棵樹都串起來。
這種方式比較常用,大部分二叉樹程式碼都是通過這種方式實現的。
順序儲存
二叉樹的順序儲存結構是基於陣列實現的,用一維陣列儲存二叉樹中的結點,並且陣列的下標能夠體現出二叉樹結點之間的邏輯關係。
在這個儲存二叉樹結點的陣列中,為了使得後續的結點邏輯關係易於理解,下標為 0 的儲存位置是不使用的。一般是把根結點儲存在 i = 1 的位置上,它的左子結點儲存在 2i = 2 的位置上、右子結點儲存在 2i + 1 = 3 的位置上。以此類推,左子結點的左子結點儲存在 2i = 4 的位置,它的右子結點儲存在 2i + 1 = 5 的位置。
總結二叉樹結點在陣列中的邏輯關係:如果結點 x 儲存在陣列中下標為 i 的位置,則結點 x 的左子結點儲存在陣列中下標為 2i 的位置,右子結點儲存在陣列中下標為 2i+1 的位置。
可以發現,上述展示的是一個完全二叉樹,使用陣列儲存完全二叉樹時,會發現除了下標為 0 的位置沒有儲存資料之外,其他的位置都被填滿了。
而如果是非完全二叉樹,則會出現浪費陣列中記憶體空間的情況。如下圖所示:
因此,一般使用順序儲存結構儲存完全二叉樹,在這種情況下,相比較鏈式儲存結構會更節省記憶體。
堆其實就是一種完全二叉樹,最常用的儲存方式就是陣列。
二叉樹的遍歷
二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中的所有結點,使得某個結點僅且被訪問一次。
深度優先遍歷
深度優先遍歷方式是指儘可能深地搜尋樹的分支,即先遍歷到葉子結點再更改搜尋路徑。二叉樹經典的深度優先遍歷方式有三種:前序遍歷、中序遍歷、後序遍歷。
其中,前、中、後序,表示的是結點與它的左右子樹結點遍歷列印的先後順序:
- 前序遍歷是指,對於樹中的任意結點來說,先列印這個結點,然後再列印它的左子樹,最後列印它的右子樹
- 中序遍歷是指,對於樹中的任意結點來說,先列印它的左子樹,然後再列印它本身,最後列印它的右子樹
- 後序遍歷是指,對於樹中的任意結點來說,先列印它的左子樹,然後再列印它的右子樹,最後列印這個結點本身
其實,二叉樹的前、中、後序遍歷就是一個遞迴的過程。比如,前序遍歷就是先列印根結點,然後再遞迴地列印左子樹,最後遞迴地列印右子樹。
下述是遞迴實現前、中、後序遍歷的虛擬碼展示:
void preOrder(Node* root) {
if (root == null) return;
// 列印根結點
print root;
// 遞迴列印左子樹
preOrder(root->left);
// 遞迴列印右子樹
preOrder(root->right);
}
void inOrder(Node* root) {
if (root == null) return;
// 遞迴列印左子樹
inOrder(root->left);
// 列印根結點
print root;
// 遞迴列印右子樹
inOrder(root->right);
}
void postOrder(Node* root) {
if (root == null) return;
// 遞迴列印左子樹
postOrder(root->left);
// 遞迴列印右子樹
postOrder(root->right);
// 列印根結點
print root;
}
除了使用遞迴的方式實現深度優先遍歷外,可以使用棧這種資料結構以非遞迴方式實現,前序遍歷方式如下:
- 將 A 結點壓入棧中,棧的結構是 [A];
- 將 A 結點彈出,然後將 A 結點的子結點 B、C 壓入棧中,棧的結構是 [C, B];
- 將 B 結點彈出,然後將 B 結點的子結點 D、E 壓入棧中,棧的結構是 [C, E, D];
- 將 D 結點彈出,D 結點沒有子結點,無需做處理,棧的結構是 [C, E];
- 將 E 結點彈出,E 結點沒有子結點,無需做處理,棧的結構是 [C];
- 將 C 結點彈出,依次類推,最終遍歷完成。
廣度優先遍歷
廣度優先遍歷又稱為層次遍歷,從上往下對每一層依次訪問,在每一層中,從左往右(也可以從右往左)訪問結點,訪問完一層再訪問下一層。
層次遍歷需要使用到佇列這種資料結構,佇列的特點是先進先出。整個遍歷過程如下:
- 將 A 結點入隊,佇列的結構是 [A];
- 將 A 結點出隊,然後將 A 結點的子結點 B、C 入隊,佇列的結構是 [B, C];
- 將 B 結點出隊,然後將 B 結點的子結點 D、E 入隊,佇列的結構是 [C、D、E];
- 將 C 結點出隊,然後將 C 結點的子結點 F、G 入隊,佇列的結構是 [D、E、F、G];
- 將 D 結點出隊,D 結點沒有子結點,無需做處理,棧的結構是 [E、F、G];
- 以此類推,最終遍歷完成。
優缺點
對於深度優先遍歷演算法,都是優先搜尋完一顆子樹,有著記憶體佔用相對較小的優點,通常儲存結點數是數的深度;非遞迴的深度優先遍歷方式會進行回溯,相對效率比較低。
對於廣度優先遍歷演算法,對於解決最短或最小問題特別有效,而且結點只訪問一遍,效率相對較高;使用廣度優先演算法需要儲存一層結點的狀態,記憶體佔用相對較高。