資料結構 - 樹
原文連結: blog.wangriyu.wang/2018/06-Tre…
與資料庫相關的樹結構主要為 B 類樹,B 類樹通常用於資料庫和作業系統的檔案系統
在學習 B 類樹之前先複習一下二叉查詢樹的概念和紅黑樹
二叉樹
二叉樹 - Binary Tree 是每個節點最多隻有兩個分支(即不存在分支度大於 2 的節點)的樹結構。
分類
- 完美二叉樹 (Perfect Binary Tree): 除了葉子結點之外的每一個結點都有兩個孩子,每一層(當然包含最後一層)都被完全填充
- 完全二叉樹 (Complete Binary Tree): 除了最後一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊
- 滿二叉樹 (Full/Strictly Binary Tree): 除了葉子結點之外的每一個結點都有兩個孩子結點
遍歷
- 前序遍歷: 首先訪問根結點然後遍歷左子樹,最後遍歷右子樹。在遍歷左、右子樹時,仍然先訪問根結點,然後遍歷左子樹,最後遍歷右子樹
- 中序遍歷: 首先遍歷左子樹,然後訪問根結點,最後遍歷右子樹。在遍歷左、右子樹時,仍然先遍歷左子樹,再訪問根結點,最後遍歷右子樹
- 後序遍歷: 首先遍歷左子樹,然後遍歷右子樹,最後訪問根結點。在遍歷左、右子樹時,仍然先遍歷左子樹,然後遍歷右子樹,最後遍歷根結點
- 深度優先搜尋: 顧名思義,查詢時深度優先,從根結點訪問最遠的結點直到找到所有節點。前序,中序和後序遍歷都是深度優先遍歷的特例
- 廣度優先搜尋: 廣度優先遍歷會先訪問離根節點最近的節點,二叉樹的廣度優先遍歷又稱按層次遍歷。演算法藉助佇列實現
二叉查詢樹
二叉查詢樹 - Binary Search Tree : 也稱二叉搜尋樹、有序二叉樹。對於根樹和所有子樹都滿足,每個節點都大於左子樹元素,而小於右子樹元素, 且沒有鍵值相等的結點
搜尋、插入、刪除的複雜度等於 樹高 ,期望 ,最壞 O(n)(數列有序,樹退化成線性表)
二叉查詢樹動態展示:visualgo.net/zh/bst
缺陷
當資料基本有序時,二叉查詢樹會退化成線性表,查詢效率嚴重下降
所以後面出現了很多改進的平衡樹結構以滿足樹高最壞也為 , 如伸展樹 (Splay Tree)、平衡二叉樹 (SBT)、AVL 樹、紅黑樹等
紅黑樹
紅黑樹 - Red–black tree 是一種自平衡二叉查詢樹,除了符合二叉查詢樹的性質外,它還滿足以下五條性質:
- 每個結點要麼是紅的,要麼是黑的
- 根結點是黑的
- 每個葉子結點是黑的(葉子結點指樹尾端 NIL 指標或 NULL 結點,不包含資料,只充當樹在此結束的指示)
- 如果一個結點是紅的,那麼它的兩個子節點都是黑的 (從根到每個葉子的所有路徑上不能有兩個連續的紅色節點)
- 對於任一結點而言,其到葉結點樹尾端 NIL 指標的每一條路徑都包含相同數目的黑結點

平衡優勢
上述約束確保了紅黑樹的關鍵特性: 從根到葉子的最長路徑不會超過最短路徑的兩倍
證明: 主要看性質 4 和 性質 5,假設從根到葉子的最短路徑 a 上有黑色節點 n 個,最長路徑 b 肯定是交替的紅色和黑色節點,而根據性質 5 可知從根到葉子的所有路徑都有相同數目的黑色節點, 這就表明 b 的黑色節點也為 n 個,但 b 出現的紅色節點不可能超過黑色節點個數,否則會破壞性質 4 (抽屜原理),所以從根到葉子的最長路徑不會超過最短路徑的兩倍
調整
因為每一個紅黑樹也是一個特化的二叉查詢樹,因此紅黑樹上的只讀操作與普通二叉查詢樹上的只讀操作相同。然而,在紅黑樹上進行插入操作和刪除操作會導致不再匹配紅黑樹的性質。 恢復紅黑樹的性質需要少量 的顏色變更(實際是非常快速的)和不超過三次樹旋轉(對於插入操作是兩次)。雖然插入和刪除很複雜,但操作時間仍可以保持為 次。
紅黑樹發生變更時需要 [變色] 和 [旋轉] 來調整,其中旋轉又分 [左旋] 和 [右旋]。
逆時針

- 右旋: 以 X 為支點
順時針
旋轉紅黑樹的兩個節點 X-Y,使得父節點被自己的左孩子取代,而自己下降為右孩子

旋轉過程中只需要做三次指標變更就行
插入和刪除
插入節點
插入節點的位置跟二叉查詢樹的尋找方法基本一致,如果插入結點 z 小於當前遍歷到的結點,則到當前結點的左子樹中繼續查詢,如果 z 大於當前結點,則到當前結點的右子樹中繼續查詢, 如果 z 依然比此刻遍歷到的新的當前結點小,則 z 作為當前結點的左孩子,否則作為當前結點的右孩子。而紅黑樹插入節點後,為了保持約束還需要進行調整修復(變色加旋轉)。
所以插入步驟如下: 紅黑樹按二叉查詢樹的規則找到位置後插入新節點 z,z 的左孩子、右孩子都是葉子結點 nil, z 結點初始都為紅色,再根據下述情形進行變色旋轉等操作,最後達到平衡。
- 情形 1: 如果 當前節點是根結點 ,為滿足性質 2,所以直接把此結點 z 塗為黑色
- 情形 2: 如果 當前結點的父結點是黑色 ,由於不違反性質 2 和性質 4,紅黑樹沒有被破壞,所以此時也是什麼也不做
比如上圖插入 12 時滿足情形 2:

以下情形需要作出額外調整:
- 情形 3: 如果 當前結點的父結點是紅色 且 祖父結點的另一個子結點(叔叔結點)是紅色
- 情形 4: 當前結點的父結點是紅色 , 叔叔結點是黑色 或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置 不在同側
- 情形 5: 當前結點的父結點是紅色 , 叔叔結點是黑色 或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置 在同側
下面著重講講後三種情況如何調整
情形 3
當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色
因為當前節點的父節點是紅色,所以父節點不可能是根節點,當前節點肯定有祖父節點,也就有叔叔節點
解決步驟: 將當前結點的父結點和叔叔結點塗黑,祖父結點塗紅,再把祖父結點當做新節點(即當前節點的指標指向祖父節點)重新檢查各種情形進行調整
由於對稱性,不管父結點是祖父結點的左子還是右子,當前結點是其父結點的左子還是右子,處理都是一樣的
我們插入 21 這個元素,當前節點指向 21:

此時會發現 21、22 兩個紅色相連與性質 4 衝突,但 21 節點滿足情形 3,修復後:

此時當前節點指向 21 的祖父節點,即 25。而 25 節點同樣遇到情形 3 的問題,繼續修復:

此時當前節點指向根節點,滿足情形 1,將 14 節點塗黑即可恢復紅黑樹平衡
情形 4
當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置不在同側
解決步驟:
- 如果當前節點是父節點的右子,父節點是祖父節點的左子,以當前結點的父結點做為新結點(即當前節點的指標指向父節點),並作為支點左旋
- 如果當前節點是父節點的左子,父節點是祖父節點的右子,以當前結點的父結點做為新結點(即當前節點的指標指向父節點),並作為支點右旋
在上圖的基礎上我們繼續插入 5 這個元素:

可以看出 5 是父節點的左子,而父節點是祖父節點的右子,不同側則為情形 4,將當前節點指向 5 的父節點 6,並以 6 為支點進行右旋:

此時當前節點是 6,而 6 是父節點 5 的右子,父節點 5 也是祖父節點 1 的右子,同側則轉為情形 5,繼續往下看
情形 5
當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置在同側
解決步驟:
- 首先把父結點變為黑色,祖父結點變為紅色
- 如果當前節點是父節點的左子,父節點是祖父節點的左子,以祖父結點為支點右旋
- 如果當前節點是父節點的右子,父節點是祖父節點的右子,以祖父結點為支點左旋
在上一張圖的基礎上修改節點 5 為黑色,節點 1 為紅色,再以 1 為支點左旋:

此時便恢復平衡
刪除節點
刪除節點 X 時第一步先判斷兩個孩子是否都是非空的,如果都非空,就先按二叉查詢樹的規則處理:
在刪除帶有兩個非空子樹的節點 X 的時候,我們可以找到左子樹中的最大元素(或者右子樹中的最小元素),並把這個最值 複製 給 X 節點,只代替原來要刪除的值,不改變節點顏色。
然後我們只要刪除那個被複製出值的那個節點就行,因為是最值節點所以它的孩子不可能都非空。
因為只是複製了一個值,不違反任何性質,這就把原問題轉化為 如何刪除最多有一個非空子樹的節點的問題 。它不關心這個節點是最初要刪除的節點還是被複製出值的那個節點。
我們以圖為例,圖中三角形代表可能為空的子樹:

節點 X 是要刪除的節點,發現它的兩個子樹非空,我們可以找左子樹中最大的元素 Max (也可以找右子樹中最小的元素 Min),把 Max 值(或者 Min 值)複製到 X 上覆蓋原來的值,不修改其他屬性,然後刪除 Max 節點(或 Min 節點)即可,可以很清楚的看到最值節點最多隻會有一個非空子樹
接下來就是如何處理刪除最多有一個非空子樹的節點 X 的問題
簡單情形:
- 如果 X 的兩個兒子都為空,即均為葉子,我們將其中任意一個看作它的兒子
- 如果 X 是一個紅色節點 ,它的父親和兒子一定是黑色的,所以簡單的 用它的黑色兒子替換它 就行,這並不會破壞性質 3 和性質 4,通過被刪除節點的所有路徑只是少了一個紅色節點,這樣可以繼續保證性質 5
- 如果 X 是黑色而它的兒子是紅色 ,如果只是刪除這個黑色節點, 用它的紅色兒子代替 的話,會破壞性質 5,我們可以 重繪它的兒子為黑色 ,則曾經通過 X 的所有路徑將通過它的黑色兒子,這樣可以繼續保持性質 5
如果 X 和它的兒子都是黑色,這是一種複雜的情況,我們單拎出來講
我們首先把要刪除的節點 X 替換為它的兒子。出於方便,稱呼這個新上位的兒子為 N,稱呼它的兄弟為 S,使用 P 稱呼 N 的新父親,SL 稱呼 S 的左兒子,SR 稱呼 S 的右兒子
有以下六種情形需要考慮:
情形 1
N 是新的根
我們不需要做什麼,因為所有路徑都去除了一個黑色節點,而新根也是黑色的,所以性質都保持著
情形 2、5、6 涉及到左右不同的情況,只取一種處理
情形 2
S 是紅色
- 交換兄弟 S 和父親 P 的顏色
- 如果 N 是其父親的左節點,我們在 N 的父親上做左旋,把紅色兄弟轉換成 N 的祖父
- 如果 N 是其父親的右節點,我們在 N 的父親上做右旋,把紅色兄弟轉換成 N 的祖父

完成這兩個操作後,儘管所有路徑上黑色節點的數目沒有改變,但現在 N 有了一個黑色的兄弟和一個紅色的父親,所以我們可以接下去按情形 4、情形 5 或情形 6 來處理
情形 3
N 的父親、S 和 S 的兒子都是黑色的
- 重繪 S 為紅色
- 將 P 作為新的 N,從情形 1 開始,在 P 上做平衡處理

在這種情形下,我們簡單的重繪 S 為紅色。結果是通過 S 的所有路徑都少了一個黑色節點。這與刪除 N 的初始父親 X 造成通過 N 的所有路徑少了一個黑色節點達成平衡。但是,通過 P 的所有路徑現在比不通過 P 的路徑少了一個黑色節點,所以仍然違反性質 5。要修正這個問題,我們要從情形 1 開始,在 P 上做重新平衡處理
情形 4
S 和 S 的兒子都是黑色,但是 N 的父親是紅色
- 交換 N 的兄弟 S 和父親 P 的顏色

在這種情形下,我們簡單的交換 N 的兄弟和父親的顏色。這不影響不通過 N 的路徑的黑色節點的數目,但是它在通過 N 的路徑上對黑色節點數目增加了一,添補了在這些路徑上刪除的黑色節點
情形 5
S 是黑色,S 的其中一個兒子是紅色,且紅色兒子的位置與 N 相對於父親的位置處於 同側
- 如果 N 是其父親的左節點,S 的左兒子是紅色,右兒子是黑色,則在 S 上做右旋轉
- 如果 N 是其父親的右節點,S 的左兒子是黑色,右兒子是紅色,則在 S 上做左旋轉
- 將 S 和它之前的紅色兒子交換顏色

所有路徑仍有同樣數目的黑色節點,但是現在 N 有了一個黑色兄弟,且兄弟的一個兒子仍為紅色的,其位置與 N 相對於父親的位置處於不同側,進入情形 6
情形 5、6 中父節點 P 的顏色可以為黑色也可以是紅色
情形 6
S 是黑色,S 的其中一個兒子是紅色,且其位置與 N 相對於父親的位置處於 不同側
- 交換 N 的父親 P 和 S 的顏色
- 如果 N 是其父親的右節點,S 的左兒子是紅色,右兒子是黑色,則在 N 的父親上做右旋轉,並使 S 的左兒子塗黑
- 如果 N 是其父親的左節點,S 的左兒子是黑色,右兒子是紅色,則在 N 的父親上做左旋轉,並使 S 的右兒子塗黑

交換前 N 的父親可以是紅色也可以是黑色,交換後,N 增加了一個黑色祖先,所以通過 N 的路徑都增加了一個黑色節點,S 的右子樹黑色節點個數也沒有變化,達到平衡
例項
還是以之前的圖為例

我們自下而上開始嘗試刪除每一個節點:
-
假如要刪除元素 1,根據簡單情形中的第二條,我們直接刪除 1,並用一個 nil 節點代替即可,元素 6、12、21 的處理與此相同
-
假如要刪除元素 5,因為左右子樹均不為空,所以找左子樹的最大值 1 (或者右子樹的最小值 6),用找到的值代替 5 (這裡只是值替換,其他均不變),然後去刪除 1 節點,這就轉到問題 1 上了
-
假如要刪除元素 11,根據簡單情形的第三條,我們直接刪除 11,並用子節點 12 代替,同時把 12 塗黑即可,元素 22 的處理與此相同
-
假如要刪除元素 25,因為左右子樹均不為空,所以找左子樹的最大值 22 (或者右子樹的最小值 27),我們這裡用值 22 代替 25,顏色不變。然後去刪除 22 節點,這變成上一個問題了
-
假如要刪除元素 27,黑色的 nil 葉子節點代替 27 節點,因為兄弟節點 22 有一個紅色孩子,且在左邊,和 nil 節點相對父親 25 的位置不同側,屬於情形 6,所以第一步交換 22 和 25 的顏色,再以 25 為支點做右旋轉,然後將 21 節點塗黑即可
-
假如要刪除元素 8,選擇右子樹最小值 11 替換 8。然後去刪除節點 11,對應問題 3
-
假如要刪除元素 17,選擇左子樹最大值 15 替換 17。然後去刪除節點 15,過程看下一個問題
-
假如要刪除元素 15,刪除的元素和替代的元素都是黑色,這屬於複雜情形。檢查其型別可以匹配到情形 2,元素 15 是被移除的 X,代替它的是 nil 節點,即為 N,17 為 P,25 為 S,根據上文可知第一步先交換 P 和 S 的顏色,然後以 P 為支點進行左旋,此時 N 多了一個黑色的兄弟 22 和紅色的父親 17:

此時 N 的兄弟 S 變為 22,P 變為 17,S 的左孩子是紅色的 21,屬於情形 5。S 做右旋轉,並交換 22 和 21 的顏色:

此時 N 的兄弟 S 變為黑色的 21,但 21 的紅色孩子節點 22 變為右側,進入情形 6

P 節點 17 做左旋轉,並將 S 的右節點塗黑,此時樹恢復平衡
- 假如要刪除根節點 14,取左子樹最大值 12 代替 14。然後去刪除節點 12,對應問題 1
至此,我們已經把節點都刪了個遍,相信你對紅黑樹的刪除操作應該瞭解了
紅黑樹動態展示: www.cs.usfca.edu/~galles/vis…
實際問題
紅黑樹還是典型的二叉搜尋樹結構,主要應用在一些 map 和 set 型別的實現上,比如 Java 中的 TreeMap 和 C++ 的 set/map/multimap 等。其查詢的時間複雜度 與樹的深度相關,降低樹的深度可以提高查詢效率。
Java 的 hashmap 和 golang 的 map 是用雜湊實現的
但是大規模資料儲存中,實現索引查詢這樣一個實際背景下,樹節點儲存的元素數量是有限的(如果元素數量非常多的話,查詢就退化成節點內部的線性查找了), 這樣導致二叉查詢樹結構由於樹的深度過大而造成磁碟 I/O 讀寫過於頻繁,進而導致查詢效率低下,因此我們該想辦法降低樹的深度,從而減少磁碟查詢存取的次數。
一個基本的思想就是:採用 多叉樹結構
,所以出現了下述的平衡多路查詢樹
B 樹 (B - Tree)
B-樹,即為 B 樹,不要讀作 B 減樹
B 樹與紅黑樹最大的不同在於,B 樹的結點可以有許多子女,從幾個到幾千個。
定義
B 樹的定義有兩種,一種以階數為限制的 B 樹(下文所述的),一種以度數為限制的 B 樹(演算法導論所描述的),兩者原理類似,這裡以階數來定義
B 樹屬於平衡多路查詢樹。一棵 m 階(m 階即代表樹中任一結點最多含有 m 個孩子)的 B 樹的特性如下:
- 除根節點外所有節點關鍵字個數範圍: [ -1, m-1]
- 若非葉子節點含 n 個關鍵字,則子樹有 n+1 個,由關鍵字範圍可知子樹的個數範圍: [ , m]
- 根節點至少包含一個關鍵字,至少有兩個孩子(除非 B 樹只存在一個節點: 根結點),即根節點關鍵字個數範圍: [1, m-1],孩子數範圍: [2, m]
- 所有葉子節點都處在同一層,即高度都一樣
- 每個節點中的關鍵字從小到大排列,節點當中 k-1 個元素正好是 k 個孩子包含的元素的值域劃分

如圖是一個典型的2-3-4 樹結構,也是階為 4 的 B 樹。從圖中查詢元素最多隻需要 3 次磁碟 I/O 就可以訪問到我們需要的資料節點,將節點資料塊讀入記憶體後再查詢指定元素會很快。如果同樣的資料用紅黑樹表示,樹高會增長很多,造成遍歷節點的次數增多,訪問磁碟的次數增多,查詢效能會下降。
對於一棵包含 n 個元素、高度為 h 、階數為 m 的 B 樹: 影響 B 樹高度的是每個結點所包含的子樹數,如果儘可能使結點孩子數都等於 ,則層數最多,為最壞情況;如果儘可能使結點孩子數都等於 m,則層數最少,為最好情況。所以有
底數 可以取很大,比如 m 可以達到幾千,從而在關鍵字數一定的情況下,使得最終的 h 值儘量比較小,樹的高度比較低。
實際運用中 B 樹中的每個結點根據實際情況可以包含大量的關鍵字資訊和分支(但不能超過磁碟塊的大小,根據磁碟驅動的不同,一般塊的大小在 1k~4k 左右);這樣樹的深度降低了,意味著查詢一個元素只要很少的結點從外存磁碟中讀入記憶體,就可以很快地訪問到要查詢的資料
查詢
一個節點的結構可以定義為:
type BTNode struct { KeyNumint// 關鍵字個數,math.Ceil(m/2)-1 <= KeyNum < 階數 m Parent*BTNode// 指向父節點的指標 IsLeafbool// 是否為葉子,葉子節點 children 為 nil Key[]int// 關鍵字切片,長度為 KeyNum Children []*BTNode // 子節點指標切片,長度為 KeyNum+1 } 複製程式碼
以上面 2-3-4 樹的根節點為例:

所有資料以塊的方式儲存在外磁碟中,我們通過 B 樹來查詢資料時,每遍歷到一個節點,便將其讀入記憶體,比較其中的關鍵字,若能匹配到我們要找的元素,便返回;若未能找到,通過比較確定在哪兩個關鍵字的值域區間, 即可確定子樹的節點指標,繼續往下找,把下一個節點的資料讀入記憶體,重複以上步驟
插入
對於一棵 m 階的 B 樹來說,插入一個元素(或者叫關鍵字)時,首先判斷在 B 樹中是否已存在,如果存在則不插入;如果不存在,則在對應葉子結點中插入新的元素,需要判斷是否會超出關鍵字個數限制(m-1)
插入步驟:
- 根據元素大小查詢插入位置,肯定是最底層的葉子節點,將元素插入到該節點中
- 如果葉子節點的關鍵字個數小於等於 m-1,說明未超出限制,插入結束;否則進入下一步
- 如果葉子節點的關鍵字個數大於 m-1 個,以結點中間的關鍵字為中心分裂成左右兩部分,然後將這個中間的關鍵字插入到父結點中,這個關鍵字的左子樹指向分裂後的左半部分,這個關鍵字的右子樹指向分裂後的右半部分。
- 然後將當前結點指向父結點,如果插入剛才的中間關鍵字後父節點的關鍵字個數也超出限制,繼續進行第 3 步;否則結束插入
還是以上面的 2-3-4 樹(階數 m = 4)為例,我們依次插入元素
- 首先插入 1、2、3,因為關鍵字個數均未超過 m-1,所以直接插入即可:

- 當插入 4 時,該節點關鍵字個數達到 m,需要分裂,這裡可以選 3 (也可以選 2) 作為中間字,分裂後:

- 繼續插入 5、7,對應 4 所在的葉子節點:

- 當插入 8 時,也需要分裂,將中間字 5 上移至父節點,4 成為 5 的左區間子樹,7 8 成為 5 的右區間子樹:

之後的步驟類似,不再一一敘述
刪除
刪除操作是指刪除 B 樹中的某個節點中的指定關鍵字
刪除步驟:
- 如果當前要刪除的關鍵字位於非葉子結點 N 上,則用後繼最小關鍵字(找前繼最大關鍵字也可以)覆蓋要刪除的關鍵字,然後在後繼關鍵字所在的子樹中刪除該後繼關鍵字。此後繼關鍵字一定位於葉子結點上,這個過程和二叉搜尋樹刪除結點的方式類似。刪除這個後繼關鍵字後進入第 2 步,如果原本要刪除的關鍵字本身就位於葉子上同樣刪除關鍵字後進入第二步
- 該結點(假設為 M)關鍵字個數大於等於 math.Ceil(m/2)-1,結束刪除操作,否則進入第 3 步
- 此時結點 M 關鍵字個數小於 math.Ceil(m/2)-1
- 如果相鄰兄弟結點(左右都可以)關鍵字個數大於 math.Ceil(m/2)-1,則父結點中取一個臨近的關鍵字下移到 M,兄弟結點中取一個臨近關鍵字上移至父節點,刪除操作結束;
- 如果相鄰的兄弟節點關鍵字個數都不大於 math.Ceil(m/2)-1,將父結點中臨近的關鍵字 key 下移至 M,合併 M 和它的兄弟節點形成一個新的結點。原父結點中的 key 的兩個孩子指標就變成一個孩子指標,指向這個新結點。然後當前結點的指標指向父結點,重複第 2 步
以上面的 2-3-4 樹為例

階數為 4,節點關鍵字個數範圍應該是 [1, 3],即 math.Ceil(m/2)-1 = 1
- 刪除關鍵字 2 或者 8,不影響節點

- 刪除關鍵字 4,該葉子節點 X 關鍵字個數變為 0 小於範圍下界,同時左右兩個相鄰兄弟的關鍵字個數都不大於 1,需要合併節點。
- 第一步,將父節點的 5 下移到 X 上
- 第二步,合併 X 和右兄弟節點 7 形成一個包含 5、7 的新節點
- 第三步,父節點中原本 5 的左右兩個孩子指標變為一個並指向這個新節點

這裡第一步也可以選擇下移 3,然後第二步跟左兄弟合併成 1、3 節點
- 繼續刪除 1,此時與上一個問題不同,該葉子節點的兄弟有富餘的關鍵字,我們只需要把父節點的臨近的一個關鍵字下移到該葉子節點代替刪除的元素,然後把兄弟節點的一個臨近關鍵字上移至父節點即可,這個操作有點類似紅黑樹的左旋操作

- 現在嘗試刪除非葉子節點 5,用後繼最小關鍵字 7 代替 5,然後刪除 7 所在的葉子節點。
- 此時會引起連鎖反應,7 所在的葉子節點現在為空,而兄弟節點關鍵字又不大於 1,需要合併
- 將關鍵字 7 又從父節點移至原來的葉子上,合併成含 3、7 的新節點,假設新節點為 N,父節點的孩子指標變為一個並指向 N
- 而父節點現在關鍵字是空的,而且其兄弟(N 的叔叔)關鍵字也不大於 1,也需要合併
- 根節點取出關鍵字 9 下移到 N 的父節點上,合併 N 的父節點和叔叔節點,產生一個包含 9、15 的新節點,根節點的孩子指標減少一個且左子樹指向這個新節點

刪除操作就演示到這,B 樹的內容講完
B 樹動態展示: www.cs.usfca.edu/~galles/vis…
B+ 樹 (B+ - Tree)
B+ 樹 是基於 B 樹的變體,查詢效能更好
同為 m 階的 B+ 樹與 B 樹的不同點:
- 所有非葉子節點,每個節點最多有 m 個關鍵字,最少有 個關鍵字(比 B 樹的限制多一個),其中每個關鍵字對應一棵子樹
- 所有的非葉子結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字,不包含關鍵字資料的指標(B 樹是包含這個指標的)
- 所有的葉子結點中包含了全部關鍵字的資訊,及指向含這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小到大順序連結.(而 B 樹的全部關鍵字資訊分散在各個節點中)

如圖所示的是將之前的 2-3-4 樹的資料存到 B+ 樹結構中的示意圖,葉子節點儲存了所有關鍵字資訊並且葉子節點之間也用指標連線起來(一個順序連結串列),而所有非葉子節點只包含子樹根節點中對應的最大關鍵字,其作用只是用於索引
B+ 樹還可以用另一種形式定義:
中間節點最多有 m-1 個關鍵字,最少有 個關鍵字,與 B 樹相同; 但是非葉子節點的關鍵字是左子樹的最大關鍵字(或者右子樹的最小關鍵字),與剛才的情形不同
比如同樣的資料此定義的 B+ 樹表現形式如下:

這種形式中間節點佔用更少,可能更常見一點,不過下面的講解是按第一種定義來
優勢
B+ 樹比 B 樹更適合實際應用中作業系統的檔案索引和資料庫索引
- B+ 樹索引節點可以儲存更多的關鍵字,磁碟 I/O 可以更少
資料庫中關鍵字可能只是某個資料列的索引資訊(比如以 ID 列建立的索引),而索引指向的資料記錄(某個 ID 對應的資料行)我們稱作 衛星資料 ,推薦看下博文資料庫的最簡單實現 和 SQL/">MySQL索引背後的資料結構及演算法原理
B- 樹中間節點和葉子節點都會帶有關鍵字和衛星資料的指標,B+ 樹中間節點只帶有關鍵字,而衛星資料的指標均放在葉子節點中
因為沒有衛星資料的指標,所以 B+ 樹內部結點相對 B 樹佔用空間更小。如果把所有同一結點的關鍵字存放在同一盤塊中,那麼對於 B+ 樹來說盤塊所能容納的關鍵字數量也就更多,一次性讀入記憶體中時能查詢的關鍵字也就更多。相對來說 IO 讀寫次數也就降低了,效能就提升了。
舉個例子,假設磁碟中的一個盤塊能容納 16 bytes,而一個關鍵字佔 2 bytes,一個衛星資料指標佔 2bytes。對於一棵 9 階 B 樹來說,一個結點最多含 8 個關鍵字(8*4 bytes),即一個內部結點需要 2 個盤塊來儲存。而對於 B+ 樹來說,內部結點不含衛星資料的指標,所以一個內部節點只需要 1 個盤塊。當需要把內部結點讀入記憶體中的時候,B 樹就比 B+ 樹多一次盤塊查詢時間
- B+ 樹的查詢效率更加穩定
由於非葉子節點並不是最終指向檔案內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查詢必須走一條從根結點到葉子結點的路徑。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。而 B 樹查詢一個檔案時查詢到的路徑長度是不一的。
- B+ 樹對範圍查詢操作更友好
如果是查詢單一元素,B+ 樹的查詢過程與 B 樹類似,只是每次查詢都是從根查到葉
而進行範圍查詢的操作時,B+ 樹只要遍歷葉子節點就可以實現整棵樹的遍歷,而 B 樹的範圍查詢要通過中序遍歷,效率比較低下
插入
B+ 樹的插入與 B 樹類似,先尋找關鍵字對應的位置插入,需要注意的是插入比當前子樹的最大關鍵字還大的數時要修改祖先節點對應的關鍵字,因為 B+ 樹內部結點存的是子樹的最大關鍵字
比如在上面給出的 B+ 樹中插入 105 這個元素,因為 105 大於當前子樹最大關鍵字 101,所以需要修改父節點和祖父節點的邊界關鍵字:

- 如果插入元素的節點未超出上界限制,則結束;否則將節點分裂,中間節點上移到父節點中,再判斷父節點是否需要調整
比如剛才插入 105 的葉子節點關鍵字個數達到 4 個,需要分裂,這裡分裂與 B 樹略有不同。B 樹是把節點按中間節點分成三份,再把中間節點上移;而 B+ 樹是分成兩份,再把左半節點的最大關鍵字新增進父節點

此時父節點也需要分裂

根節點未超出 4,結束;假如此時根節點也超出上界了,需要把根節點也分裂,生成一個新的根節點,且新的根節點的關鍵字為左右子樹的最大關鍵字
刪除
B+ 樹的刪除與 B 樹也類似,找到要刪除的關鍵字,如果是當前子樹的最大關鍵字,刪除該關鍵字後還要修改祖先節點對應的關鍵字;如果不是當前子樹的最大關鍵字,直接刪除;
在上一張圖的基礎上刪除 8,這是葉子的最大關鍵字,所以需要修改父節點和祖父節點的邊界關鍵字:

- 如果刪除元素的節點未低於下界限制,則結束;否則分兩種情況處理:
- 如果兄弟節點有富餘關鍵字,則從兄弟節點中移動一個關鍵字到當前節點,修改父節點對應邊界關鍵字即可
- 如果兄弟節點關鍵字個數都處於下界值,不能外借元素,則合併當前節點和兄弟節點,修改父節點的孩子指標以及邊界關鍵字,此時父節點關鍵字個數也少了一個,將當前節點的指標指向父節點繼續判斷處理
我們繼續刪除 7,此時該葉子節點關鍵字個數少於 1 需要調整,而兄弟節點有富餘關鍵字,可以移動 5 到當前節點,修改父節點和祖父節點的邊界關鍵字

繼續刪除 5,兄弟節點的關鍵字個數為下界值 1,不能外借,則合併當前節點和兄弟節點,並修改父節點指標及關鍵字,相應的祖父節點也需要修改邊界關鍵字

B+ 樹動態展示: www.cs.usfca.edu/~galles/vis…
B* 樹 (B* - Tree)
B* 樹是 B+ 樹的變體,在 B+ 樹的基礎上(所有的葉子結點中包含了全部關鍵字的資訊,及指向含有這些關鍵字記錄的指標),B* 樹多了兩條性質:
- 中間結點也增加了指向兄弟的指標,即每一層節點都可以橫向遍歷
- B* 樹定義了非葉子結點關鍵字個數至少為 ,即塊的最低使用率為 2/3,代替 B+ 樹的 1/2
下圖的資料與之前 B+ 樹的資料一樣,但分支結構有所不同(因為中間節點關鍵字範圍變為[3, 4],不同於之前 B+ 樹的 [2, 4]),而且第二層節點之間也用指標連線起來

優勢
B+ 樹節點滿時就會分裂,而 B* 樹節點滿時會先檢查兄弟節點是否滿(因為每個節點都有指向兄弟的指標):
- 如果兄弟節點未滿則向兄弟節點轉移關鍵字,然後修改原節點和兄弟結點的關鍵字以及會受最大關鍵字變動影響的祖先的邊界關鍵字
- 如果兄弟節點已滿,則從當前節點和兄弟節點各拿出 1/3 的資料建立一個新的節點出來,然後在父結點增加新結點的指標
B* 樹存有兄弟節點的指標,可以向兄弟節點轉移關鍵字的特性使得 B* 樹分解次數變得更少,節點空間使用率更高
因為沒有找到相關的內容,關於 B* 樹的插入刪除這裡不再講解
總結
本文依次介紹了二叉樹 -> 二叉搜尋樹 -> 平衡二叉搜尋樹(紅黑樹) -> 平衡多路查詢樹(B 類樹),各有特點,其中 B 類樹是介紹的重點,因為實際運用中索引結構使用的是 B 類樹
因為樹的上面幾層會反覆查詢,所以我們可以把樹的前幾層存在記憶體中,而底層的資料存在外部磁盤裡,這樣效率更高
當然 B 樹也存在弊端:
因為一旦確定最大階數,後面的使用過程中就不可以修改關鍵字個數的範圍
那麼除非完全重建資料庫,否則無法改變鍵值的最大長度。這使得許多資料庫系統將人名截斷到 70 字元之內
後面一篇我們會講解另一種 Mysql 的索引結構: 雜湊索引,可以動態適應任意長度的鍵值