二叉樹的簡單認識


樹的定義

樹的結構

樹是一種抽象數據類型,用來模擬具有樹狀結構性質的數據集合。樹的專業術語比較多,需要了解一下:

  • 樹的結點:包含一個數據元素及若干指向子樹分支的信息
  • 結點的度:一個結點含有的子樹數目稱為該結點的度
  • 樹的度:樹中最大的結點度稱為樹的度
  • 葉子結點:也稱終端結點,結點度為零的結點
  • 分支結點:也稱非終端結點,結點度不為零的結點
  • 子結點:一個結點含有的子樹的根結點稱為該結點的子結點
  • 父結點:若一個結點含有子結點,則這個結點稱為其子結點的父結點
  • 兄弟結點:具有相同父結點的結點互稱為兄弟結點
  • 堂兄弟結點:父結點在同一層的結點互為堂兄弟結點
  • 結點的祖先:從根到該結點所經分支上的所有結點稱為該結點的祖先
  • 子孫:以某結點為根的子樹中任一結點都稱為該結點的子孫
  • 結點的層次:從根開始定義起,根為第 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;
}

除了使用遞歸的方式實現深度優先遍歷外,還可以使用棧這種數據結構以非遞歸方式實現,前序遍歷方式如下:

  1. 將 A 結點壓入棧中,棧的結構是 [A];
  2. 將 A 結點彈出,然后將 A 結點的子結點 B、C 壓入棧中,棧的結構是 [C, B];
  3. 將 B 結點彈出,然后將 B 結點的子結點 D、E 壓入棧中,棧的結構是 [C, E, D];
  4. 將 D 結點彈出,D 結點沒有子結點,無需做處理,棧的結構是 [C, E];
  5. 將 E 結點彈出,E 結點沒有子結點,無需做處理,棧的結構是 [C];
  6. 依次類推,最終以 A、B、D、E、C、F、G 的次序彈出結點元素。

廣度優先遍歷

廣度優先遍歷又稱為層次遍歷,從上往下對每一層依次訪問,在每一層中,從左往右(也可以從右往左)訪問結點,訪問完一層再訪問下一層。

層次遍歷需要使用到隊列這種數據結構,隊列的特點是先進先出。整個遍歷過程如下:

  1. 將 A 結點入隊,隊列的結構是 [A];
  2. 將 A 結點出隊,然后將 A 結點的子結點 B、C 入隊,隊列的結構是 [B, C];
  3. 將 B 結點出隊,然后將 B 結點的子結點 D、E 入隊,隊列的結構是 [C, D, E];
  4. 將 C 結點出隊,然后將 C 結點的子結點 F、G 入隊,隊列的結構是 [D, E, F, G];
  5. 將 D 結點出隊,D 結點沒有子結點,無需做處理,棧的結構是 [E, F, G];
  6. 以此類推,最終 A、B、C、D、E、F、G 的次序彈出結點元素。

優缺點

對於深度優先遍歷算法,都是優先搜索完一顆子樹,有着內存占用相對較小的優點,通常存儲結點數是數的深度;非遞歸的深度優先遍歷方式會進行回溯,相對效率比較低。

對於廣度優先遍歷算法,對於解決最短或最小問題特別有效,而且結點只訪問一遍,效率相對較高;使用廣度優先算法需要存儲一層結點的狀態,內存占用相對較高。


免責聲明!

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



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