資料結構 - 二叉樹

fatedeity發表於2022-03-02

樹的定義

樹的結構

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

  • 樹的結點:包含一個資料元素及若干指向子樹分支的資訊
  • 結點的度:一個結點含有的子樹的數目稱為該結點的度
  • 樹的度:樹中最大的結點度稱為樹的度
  • 葉子結點:也稱終端結點,結點度為零的結點
  • 分支結點:也稱非終端結點,結點度不為零的結點
  • 子結點:一個結點含有的子樹的根結點稱為該結點的子結點
  • 父結點:若一個結點含有子結點,則這個結點稱為其子結點的父結點
  • 兄弟結點:具有相同父結點的結點互稱為兄弟結點
  • 堂兄弟結點:父結點在同一層的結點互為堂兄弟結點
  • 結點的祖先:從根到該結點所經分支上的所有結點
  • 子孫:以某結點為根的子樹中任一結點都稱為該結點的子孫
  • 結點的層次:從根開始定義起,根為第 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. 將 C 結點彈出,依次類推,最終遍歷完成。

廣度優先遍歷

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

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

  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. 以此類推,最終遍歷完成。

優缺點

對於深度優先遍歷演算法,都是優先搜尋完一顆子樹,有著記憶體佔用相對較小的優點,通常儲存結點數是數的深度;非遞迴的深度優先遍歷方式會進行回溯,相對效率比較低。

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

相關文章