前言
因為本人天資愚鈍,所以總喜歡將抽象化的事務具象化表達。對於各類眼花繚亂的樹,只需要認知到它們只是一種資料結構,類似陣列,切片,列表,對映等這些耳熟能詳的詞彙。對於一個資料結構而言,無非就是增刪改查而已,既然各類樹也是資料結構,它們就不能逃離增刪改查的桎梏。
那麼,為什麼我們需要樹這種資料結構呢,直接用陣列不行嗎,用切片不行嗎?當然可以,只不過現實世界是繽紛雜亂的,而又沒有一種萬能藥式的資料結構以應對千變萬化的業務需求。所以,才會有各類樹,而且一些“高階”資料結構是基於樹形資料結構的,例如對映。
二叉樹
在中文語境中,節點結點傻傻分不清楚,故後文以 node 代表 "結點",root node 代表根節點,child node 代表 “子節點”
二叉樹是諸多樹狀結構的始祖,至於為什麼不是三叉樹,四叉樹,或許是因為計算機只能數到二吧,哈哈,開個玩笑。二叉樹很簡單,每個 node 最多存在兩個 child node,第一個節點稱之為 root node。
二叉樹具備著一些基本的數學性質,不過很簡單,定義從 i
從 0 開始:
- 第
i
層至多有2i
個 node; - 深度為 i 層二叉樹至多有
2i+1-1
個 node。
二叉樹的特殊型別
這裡有興趣的可以瞭解一下,不影響後文的閱讀。二叉樹根據 child node 的不同,衍生出了幾種特殊型別:在一顆二叉樹中,如果每個 node 都有 0 或 2 個 child node,則二叉樹是滿二叉樹;定義從 i
從 0 開始,一棵深度為 i
,且僅有 2i+1−1
個 node 的二叉樹,稱為完美二叉樹;若除最後一層外的其餘層都是滿的,並且最後一層要麼是滿的,要麼在右邊缺少連續若干 node,則此二叉樹為完全二叉樹。
二叉搜尋樹
二叉搜尋樹(Binary Search Tree),也叫二叉查詢樹,有序二叉樹,排序二叉樹(名字還挺多)。它是一種常用且特殊的二叉樹,它具備一個特有的性質,left node(左結點)始終小於 parent node (父結點),right node 始終大於 parent node。
二叉搜尋樹的查詢
- 二叉搜尋樹從 root node 開始,如果命中則返回;
- 否則,目標值比 node 小進入 left node;
- 比 node 大進入 right node;
- 如果左右都為空,則未命中。
二叉搜尋樹的遍歷
二叉搜尋樹有不同的遍歷方式,這裡介紹常用的中序遍歷方式:
- 先遍歷左子樹;
- 然後查詢當前左子樹的 parent node;
- 遍歷右子樹。
二叉搜尋樹的插入
- 二叉搜尋樹從 root node 開始,如果命中則不進行操作;
- 否則,目標值比 node 小進入 left node;
- 比 node 大進入 right node;
- 最終將值插入搜尋停止的地方。
二叉搜尋樹的刪除
二叉樹的刪除和查詢基本一致,只要在命中時刪除即可。
- 二叉搜尋樹從 root node 開始,如果命中則刪除;
- 否則,目標值比 node 小進入 left node;
- 比 node 大進入 right node;
- 刪除後使用該 node 左子樹最大值或者右子樹最小值替代該 node。
自平衡二叉樹
從上面的幾張動圖中我們知曉,二叉搜尋樹不同於線性結構,它可以大大降低查詢,插入的時間複雜度。但在特殊情況下,二叉搜尋樹可能退化為線性結構,假如我們依次插入1,2,3,4,5:
此時,二叉搜尋樹退化為線性結構,效率重新變回遍歷。於是,便出現了自平衡二叉樹,例如 AVL 樹,紅黑樹,替罪羊樹等。但它們並不是本文重點,下面我要介紹的是另外一種很常見的自平衡二叉樹:B樹。
B樹
B樹和B-樹是同一個概念。B樹相對於二叉樹有兩點最大的不同:
- 每個 node 可以有不止一個數值
- 每個 node 也可以有不止兩個 child node
B樹有兩種型別 node:
- internal node(內部結點):不僅僅儲存資料,也具備 child node;
- leaf node(葉子結點):僅儲存資料,不具備 child node。
這兩種 node 不同於前文所提的 root node 和 child node。root 和 child 是相對於階層的概念,而 internal 和 leaf 是相對於性質的概念
一個簡單的圖例如下:
圖中的藍色方塊是 internal node,綠色則是 leaf node。
B樹有一些需要滿足的性質,這裡的抽象的邏輯有些燒腦,我會對照前面的圖片來解釋。設定一顆 m 階的B樹,m = 3
:
設 internal node 的 child node 個數為 k
:
- 如果 internal node 是 root node,那麼
k = [2, m]
,比如上圖的 8 有兩個 child node(3|6, 10/12); - 如果 internal node 不是 root node,那麼
k = [m/2, m]
,m/2 向上取整,比如上圖的3|6
有三個 child node; - 如果 root node 的
k
為 0,那麼 root node 是 leaf 型別的; - 所有 leaf node 在同一層,上圖最後一行的六個 node。
設任意 node 鍵值個數為 n
:
- 對於 internal node,
n = k-1
, 升序排序,滿足k[i] < k[i+1]
,比如上圖的三個 internal(8,3|6,10|12) 都滿足此規律; - 對於 leaf node,
n = [0, m-1]
,同樣升序排序,比如上圖最後一個的六個 leaf,其鍵值最多為兩個。
上述的概念有些抽象,但是這是理解B樹關鍵的地方所在,後面在B樹的插入講解,會有更多具象的動圖來解釋這些概念。
B樹的查詢
B樹的查詢類似於二叉樹:
- 從 root node 開始,如果目標值小於 root node,進入左子樹,否則進入右子樹;
- 遍歷 child node 的多個鍵值;
- 如果匹配到鍵值,則返回;
- 如果不匹配,則根據目標值的範圍選擇對應的子樹;
- 重複步驟2、3、4,直到匹配成功返回或者未找到。
假如我們要查詢 11:
B樹的遍歷
B樹的遍歷方式類似二叉搜尋樹,不過因為B樹一個 node 有多個鍵值和多個 child node,所以需要遍歷每個左右子樹和鍵值:
- 先遍歷第一個左子樹,也就是 parent node 第一個鍵值的左邊;
- 然後查詢當前 parent node 的第一個鍵值;
- 遍歷第二個左子樹,也就是 parent node 第二個鍵值的左邊;
- 遍歷完搜尋的左子樹,最後遍歷當前 parent 的最右子樹,即最後一個鍵值的右邊。
B樹的插入
插入前面的過程和查詢一致,在插入後可能需要重整 node,以符合B樹的性質,例如插入 16:
- 先查詢到目標 node,也就是
13|15
; - 因為這是一顆 3 階B樹,所以 node 最多隻能有兩個鍵值,於是向上傳遞中間值 15;
- parent node 最多也只能有兩個鍵值,於是繼續向上傳遞中間值 12;
- 此時 root node 是 8|12,需要有三個 child node,於是 10|15 需要拆分,再向下進一步調整,至此,插入 16 完成。
B樹的刪除
刪除是插入的逆操作,但是往往比插入更復雜,因為刪除後經常需要重整 node:
- 先查詢到目標 node,也就是
16
; - 刪除 16,此時 15 child node 剩下一個,不符合條件,遞迴向上調整,一直到根節點;
- 直到所有的條件都滿足後,刪除 16 完成。