樹的定義
樹是一種抽象數據類型,用來模擬具有樹狀結構性質的數據集合。樹的專業術語比較多,需要了解一下:
- 樹的結點:包含一個數據元素及若干指向子樹分支的信息
- 結點的度:一個結點含有的子樹數目稱為該結點的度
- 樹的度:樹中最大的結點度稱為樹的度
- 葉子結點:也稱終端結點,結點度為零的結點
- 分支結點:也稱非終端結點,結點度不為零的結點
- 子結點:一個結點含有的子樹的根結點稱為該結點的子結點
- 父結點:若一個結點含有子結點,則這個結點稱為其子結點的父結點
- 兄弟結點:具有相同父結點的結點互稱為兄弟結點
- 堂兄弟結點:父結點在同一層的結點互為堂兄弟結點
- 結點的祖先:從根到該結點所經分支上的所有結點稱為該結點的祖先
- 子孫:以某結點為根的子樹中任一結點都稱為該結點的子孫
- 結點的層次:從根開始定義起,根為第 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];
- 依次類推,最終以 A、B、D、E、C、F、G 的次序彈出結點元素。
廣度優先遍歷
廣度優先遍歷又稱為層次遍歷,從上往下對每一層依次訪問,在每一層中,從左往右(也可以從右往左)訪問結點,訪問完一層再訪問下一層。
層次遍歷需要使用到隊列這種數據結構,隊列的特點是先進先出。整個遍歷過程如下:
- 將 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];
- 以此類推,最終 A、B、C、D、E、F、G 的次序彈出結點元素。
優缺點
對於深度優先遍歷算法,都是優先搜索完一顆子樹,有着內存占用相對較小的優點,通常存儲結點數是數的深度;非遞歸的深度優先遍歷方式會進行回溯,相對效率比較低。
對於廣度優先遍歷算法,對於解決最短或最小問題特別有效,而且結點只訪問一遍,效率相對較高;使用廣度優先算法需要存儲一層結點的狀態,內存占用相對較高。