數據結構 - 樹

原文連接: blog.wangriyu.wang/2018/06-Tre…html

與數據庫相關的樹結構主要爲 B 類樹,B 類樹一般用於數據庫和操做系統的文件系統mysql

在學習 B 類樹以前先複習一下二叉查找樹的概念和紅黑樹golang

二叉樹

二叉樹 - Binary Tree 是每一個節點最多隻有兩個分支(即不存在分支度大於 2 的節點)的樹結構。面試

分類

  • 完美二叉樹 (Perfect Binary Tree): 除了葉子結點以外的每個結點都有兩個孩子,每一層(固然包含最後一層)都被徹底填充
  • 徹底二叉樹 (Complete Binary Tree): 除了最後一層以外的其餘每一層都被徹底填充,而且全部結點都保持向左對齊
  • 滿二叉樹 (Full/Strictly Binary Tree): 除了葉子結點以外的每個結點都有兩個孩子結點

遍歷

  • 前序遍歷: 首先訪問根結點而後遍歷左子樹,最後遍歷右子樹。在遍歷左、右子樹時,仍然先訪問根結點,而後遍歷左子樹,最後遍歷右子樹
  • 中序遍歷: 首先遍歷左子樹,而後訪問根結點,最後遍歷右子樹。在遍歷左、右子樹時,仍然先遍歷左子樹,再訪問根結點,最後遍歷右子樹
  • 後序遍歷: 首先遍歷左子樹,而後遍歷右子樹,最後訪問根結點。在遍歷左、右子樹時,仍然先遍歷左子樹,而後遍歷右子樹,最後遍歷根結點
  • 深度優先搜索: 顧名思義,查找時深度優先,從根結點訪問最遠的結點直到找到全部節點。前序,中序和後序遍歷都是深度優先遍歷的特例
  • 廣度優先搜索: 廣度優先遍歷會先訪問離根節點最近的節點,二叉樹的廣度優先遍歷又稱按層次遍歷。算法藉助隊列實現

二叉查找樹

二叉查找樹 - Binary Search Tree: 也稱二叉搜索樹、有序二叉樹。對於根樹和全部子樹都知足,每一個節點都大於左子樹元素,而小於右子樹元素,且沒有鍵值相等的結點算法

搜索、插入、刪除的複雜度等於樹高,指望 O(\log_2^n),最壞 O(n)(數列有序,樹退化成線性表)sql

二叉查找樹動態展現: visualgo.net/zh/bst數據庫

缺陷

當數據基本有序時,二叉查找樹會退化成線性表,查找效率嚴重降低編程

因此後面出現了不少改進的平衡樹結構以知足樹高最壞也爲 O(\log_2^n), 如伸展樹 (Splay Tree)、平衡二叉樹 (SBT)、AVL 樹、紅黑樹等數據結構

紅黑樹

紅黑樹 - Red–black tree 是一種自平衡二叉查找樹,除了符合二叉查找樹的性質外,它還知足如下五條性質:性能

  1. 每一個結點要麼是紅的,要麼是黑的
  2. 根結點是黑的
  3. 每一個葉子結點是黑的(葉子結點指樹尾端 NIL 指針或 NULL 結點,不包含數據,只充當樹在此結束的指示)
  4. 若是一個結點是紅的,那麼它的兩個子節點都是黑的 (從根到每一個葉子的全部路徑上不能有兩個連續的紅色節點)
  5. 對於任一結點而言,其到葉結點樹尾端 NIL 指針的每一條路徑都包含相同數目的黑結點

一棵紅黑樹

平衡優點

上述約束確保了紅黑樹的關鍵特性: 從根到葉子的最長路徑不會超過最短路徑的兩倍

證實: 主要看性質 4 和 性質 5,假設從根到葉子的最短路徑 a 上有黑色節點 n 個,最長路徑 b 確定是交替的紅色和黑色節點,而根據性質 5 可知從根到葉子的全部路徑都有相同數目的黑色節點, 這就代表 b 的黑色節點也爲 n 個,但 b 出現的紅色節點不可能超過黑色節點個數,不然會破壞性質 4 (抽屜原理),因此從根到葉子的最長路徑不會超過最短路徑的兩倍

調整

由於每個紅黑樹也是一個特化的二叉查找樹,所以紅黑樹上的只讀操做與普通二叉查找樹上的只讀操做相同。然而,在紅黑樹上進行插入操做和刪除操做會致使再也不匹配紅黑樹的性質。 恢復紅黑樹的性質須要少許 O(\log_2^n) 的顏色變動(實際是很是快速的)和不超過三次樹旋轉(對於插入操做是兩次)。雖然插入和刪除很複雜,但操做時間仍能夠保持爲 O(\log_2^n) 次。

紅黑樹發生變動時須要 [變色] 和 [旋轉] 來調整,其中旋轉又分 [左旋] 和 [右旋]。

  • 變色就是更改顏色
  • 左旋: 以 X 爲支點逆時針旋轉紅黑樹的兩個節點 X-Y,使得父節點被本身的右孩子取代,而本身降低爲左孩子

左旋

  • 右旋: 以 X 爲支點順時針旋轉紅黑樹的兩個節點 X-Y,使得父節點被本身的左孩子取代,而本身降低爲右孩子

右旋

旋轉過程當中只須要作三次指針變動就行

插入和刪除

插入節點

插入節點的位置跟二叉查找樹的尋找方法基本一致,若是插入結點 z 小於當前遍歷到的結點,則到當前結點的左子樹中繼續查找,若是 z 大於當前結點,則到當前結點的右子樹中繼續查找, 若是 z 依然比此刻遍歷到的新的當前結點小,則 z 做爲當前結點的左孩子,不然做爲當前結點的右孩子。而紅黑樹插入節點後,爲了保持約束還須要進行調整修復(變色加旋轉)。

因此插入步驟以下: 紅黑樹按二叉查找樹的規則找到位置後插入新節點 z,z 的左孩子、右孩子都是葉子結點 nil, z 結點初始都爲紅色,再根據下述情形進行變色旋轉等操做,最後達到平衡。

  • 情形 1: 若是當前節點是根結點,爲知足性質 2,因此直接把此結點 z 塗爲黑色
  • 情形 2: 若是當前結點的父結點是黑色,因爲不違反性質 2 和性質 4,紅黑樹沒有被破壞,因此此時也是什麼也不作

好比上圖插入 12 時知足情形 2:

插入 12

如下情形須要做出額外調整:

  • 情形 3: 若是當前結點的父結點是紅色祖父結點的另外一個子結點(叔叔結點)是紅色
  • 情形 4: 當前結點的父結點是紅色叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置不在同側
  • 情形 5: 當前結點的父結點是紅色叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置在同側

下面着重講講後三種狀況如何調整

情形 3

當前結點的父結點是紅色且祖父結點的另外一個子結點(叔叔結點)是紅色

由於當前節點的父節點是紅色,因此父節點不多是根節點,當前節點確定有祖父節點,也就有叔叔節點

解決步驟: 將當前結點的父結點和叔叔結點塗黑,祖父結點塗紅,再把祖父結點當作新節點(即當前節點的指針指向祖父節點)從新檢查各類情形進行調整

因爲對稱性,無論父結點是祖父結點的左子仍是右子,當前結點是其父結點的左子仍是右子,處理都是同樣的

咱們插入 21 這個元素,當前節點指向 21:

插入 21

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

調整情形 3

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

繼續調整情形 3

此時當前節點指向根節點,知足情形 1,將 14 節點塗黑便可恢復紅黑樹平衡

情形 4

當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置不在同側

解決步驟:

  • 若是當前節點是父節點的右子,父節點是祖父節點的左子,以當前結點的父結點作爲新結點(即當前節點的指針指向父節點),並做爲支點左旋
  • 若是當前節點是父節點的左子,父節點是祖父節點的右子,以當前結點的父結點作爲新結點(即當前節點的指針指向父節點),並做爲支點右旋

在上圖的基礎上咱們繼續插入 5 這個元素:

插入 5

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

插入情形 4

此時當前節點是 6,而 6 是父節點 5 的右子,父節點 5 也是祖父節點 1 的右子,同側則轉爲情形 5,繼續往下看

情形 5

當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置在同側

解決步驟:

  • 首先把父結點變爲黑色,祖父結點變爲紅色
  • 若是當前節點是父節點的左子,父節點是祖父節點的左子,以祖父結點爲支點右旋
  • 若是當前節點是父節點的右子,父節點是祖父節點的右子,以祖父結點爲支點左旋

在上一張圖的基礎上修改節點 5 爲黑色,節點 1 爲紅色,再以 1 爲支點左旋:

插入情形 5

此時便恢復平衡

刪除節點

刪除節點 X 時第一步先判斷兩個孩子是否都是非空的,若是都非空,就先按二叉查找樹的規則處理:

在刪除帶有兩個非空子樹的節點 X 的時候,咱們能夠找到左子樹中的最大元素(或者右子樹中的最小元素),並把這個最值複製給 X 節點,只代替原來要刪除的值,不改變節點顏色。

而後咱們只要刪除那個被複製出值的那個節點就行,由於是最值節點因此它的孩子不可能都非空。

由於只是複製了一個值,不違反任何性質,這就把原問題轉化爲如何刪除最多有一個非空子樹的節點的問題。它不關心這個節點是最初要刪除的節點仍是被複製出值的那個節點。

咱們以圖爲例,圖中三角形表明可能爲空的子樹:

取值複製

節點 X 是要刪除的節點,發現它的兩個子樹非空,咱們能夠找左子樹中最大的元素 Max (也能夠找右子樹中最小的元素 Min),把 Max 值(或者 Min 值)複製到 X 上覆蓋原來的值,不修改其餘屬性,而後刪除 Max 節點(或 Min 節點)便可,能夠很清楚的看到最值節點最多隻會有一個非空子樹


接下來就是如何處理刪除最多有一個非空子樹的節點 X 的問題

簡單情形:

  1. 若是 X 的兩個兒子都爲空,即均爲葉子,咱們將其中任意一個看做它的兒子
  2. 若是 X 是一個紅色節點,它的父親和兒子必定是黑色的,因此簡單的用它的黑色兒子替換它就行,這並不會破壞性質 3 和性質 4,經過被刪除節點的全部路徑只是少了一個紅色節點,這樣能夠繼續保證性質 5
  3. 若是 X 是黑色而它的兒子是紅色,若是隻是刪除這個黑色節點,用它的紅色兒子代替的話,會破壞性質 5,咱們能夠重繪它的兒子爲黑色,則曾經經過 X 的全部路徑將經過它的黑色兒子,這樣能夠繼續保持性質 5

若是 X 和它的兒子都是黑色,這是一種複雜的狀況,咱們單拎出來說

咱們首先把要刪除的節點 X 替換爲它的兒子。出於方便,稱呼這個新上位的兒子爲 N,稱呼它的兄弟爲 S,使用 P 稱呼 N 的新父親,SL 稱呼 S 的左兒子,SR 稱呼 S 的右兒子

有如下六種情形須要考慮:

情形 1

N 是新的根

咱們不須要作什麼,由於全部路徑都去除了一個黑色節點,而新根也是黑色的,因此性質都保持着

情形 二、五、6 涉及到左右不一樣的狀況,只取一種處理

情形 2

S 是紅色

  • 交換兄弟 S 和父親 P 的顏色
  • 若是 N 是其父親的左節點,咱們在 N 的父親上作左旋,把紅色兄弟轉換成 N 的祖父
  • 若是 N 是其父親的右節點,咱們在 N 的父親上作右旋,把紅色兄弟轉換成 N 的祖父

刪除情形 2

完成這兩個操做後,儘管全部路徑上黑色節點的數目沒有改變,但如今 N 有了一個黑色的兄弟和一個紅色的父親,因此咱們能夠接下去按情形 四、情形 5 或情形 6 來處理

情形 3

N 的父親、S 和 S 的兒子都是黑色的

  • 重繪 S 爲紅色
  • 將 P 做爲新的 N,從情形 1 開始,在 P 上作平衡處理

刪除情形 3

在這種情形下,咱們簡單的重繪 S 爲紅色。結果是經過 S 的全部路徑都少了一個黑色節點。這與刪除 N 的初始父親 X 形成經過 N 的全部路徑少了一個黑色節點達成平衡。可是,經過 P 的全部路徑如今比不經過 P 的路徑少了一個黑色節點,因此仍然違反性質 5。要修正這個問題,咱們要從情形 1 開始,在 P 上作從新平衡處理

情形 4

S 和 S 的兒子都是黑色,可是 N 的父親是紅色

  • 交換 N 的兄弟 S 和父親 P 的顏色

刪除情形 4

在這種情形下,咱們簡單的交換 N 的兄弟和父親的顏色。這不影響不經過 N 的路徑的黑色節點的數目,可是它在經過 N 的路徑上對黑色節點數目增長了一,添補了在這些路徑上刪除的黑色節點

情形 5

S 是黑色,S 的其中一個兒子是紅色,且紅色兒子的位置與 N 相對於父親的位置處於同側

  • 若是 N 是其父親的左節點,S 的左兒子是紅色,右兒子是黑色,則在 S 上作右旋轉
  • 若是 N 是其父親的右節點,S 的左兒子是黑色,右兒子是紅色,則在 S 上作左旋轉
  • 將 S 和它以前的紅色兒子交換顏色

刪除情形 5

全部路徑仍有一樣數目的黑色節點,可是如今 N 有了一個黑色兄弟,且兄弟的一個兒子仍爲紅色的,其位置與 N 相對於父親的位置處於不一樣側,進入情形 6

情形 五、6 中父節點 P 的顏色能夠爲黑色也能夠是紅色

情形 6

S 是黑色,S 的其中一個兒子是紅色,且其位置與 N 相對於父親的位置處於不一樣側

  • 交換 N 的父親 P 和 S 的顏色
  • 若是 N 是其父親的右節點,S 的左兒子是紅色,右兒子是黑色,則在 N 的父親上作右旋轉,並使 S 的左兒子塗黑
  • 若是 N 是其父親的左節點,S 的左兒子是黑色,右兒子是紅色,則在 N 的父親上作左旋轉,並使 S 的右兒子塗黑

刪除情形 6

交換前 N 的父親能夠是紅色也能夠是黑色,交換後,N 增長了一個黑色祖先,因此經過 N 的路徑都增長了一個黑色節點,S 的右子樹黑色節點個數也沒有變化,達到平衡

實例

仍是以以前的圖爲例

刪除

咱們自下而上開始嘗試刪除每個節點:

  • 假如要刪除元素 1,根據簡單情形中的第二條,咱們直接刪除 1,並用一個 nil 節點代替便可,元素 六、十二、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:

刪除 15

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

刪除 15

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

刪除 15

P 節點 17 作左旋轉,並將 S 的右節點塗黑,此時樹恢復平衡

  • 假如要刪除根節點 14,取左子樹最大值 12 代替 14。而後去刪除節點 12,對應問題 1

至此,咱們已經把節點都刪了個遍,相信你對紅黑樹的刪除操做應該瞭解了

紅黑樹動態展現: www.cs.usfca.edu/~galles/vis…

實際問題

紅黑樹仍是典型的二叉搜索樹結構,主要應用在一些 map 和 set 類型的實現上,好比 Java 中的 TreeMap 和 C++ 的 set/map/multimap 等。其查找的時間複雜度 O(\log_2^n) 與樹的深度相關,下降樹的深度能夠提升查找效率。

Java 的 hashmap 和 golang 的 map 是用哈希實現的

可是大規模數據存儲中,實現索引查詢這樣一個實際背景下,樹節點存儲的元素數量是有限的(若是元素數量很是多的話,查找就退化成節點內部的線性查找了), 這樣致使二叉查找樹結構因爲樹的深度過大而形成磁盤 I/O 讀寫過於頻繁,進而致使查詢效率低下,所以咱們該想辦法下降樹的深度,從而減小磁盤查找存取的次數。

一個基本的思想就是:採用多叉樹結構,因此出現了下述的平衡多路查找樹

B 樹 (B - Tree)

B-樹,即爲 B 樹,不要讀做 B 減樹

B 樹與紅黑樹最大的不一樣在於,B 樹的結點能夠有許多子女,從幾個到幾千個。

定義

B 樹的定義有兩種,一種以階數爲限制的 B 樹(下文所述的),一種以度數爲限制的 B 樹(算法導論所描述的),二者原理相似,這裏以階數來定義

B 樹屬於平衡多路查找樹。一棵 m 階(m 階即表明樹中任一結點最多含有 m 個孩子)的 B 樹的特性以下:

  1. 除根節點外全部節點關鍵字個數範圍: [\lceil\frac m2\rceil-1, m-1]
  2. 若非葉子節點含 n 個關鍵字,則子樹有 n+1 個,由關鍵字範圍可知子樹的個數範圍: [\lceil\frac m2\rceil, m]
  3. 根節點至少包含一個關鍵字,至少有兩個孩子(除非 B 樹只存在一個節點: 根結點),即根節點關鍵字個數範圍: [1, m-1],孩子數範圍: [2, m]
  4. 全部葉子節點都處在同一層,即高度都同樣
  5. 每一個節點中的關鍵字從小到大排列,節點當中 k-1 個元素正好是 k 個孩子包含的元素的值域劃分

2-3-4 樹

如圖是一個典型的 2-3-4 樹結構,也是階爲 4 的 B 樹。從圖中查詢元素最多隻須要 3 次磁盤 I/O 就能夠訪問到咱們須要的數據節點,將節點數據塊讀入內存後再查找指定元素會很快。若是一樣的數據用紅黑樹表示,樹高會增加不少,形成遍歷節點的次數增多,訪問磁盤的次數增多,查找性能會降低。

對於一棵包含 n 個元素、高度爲 h 、階數爲 m 的 B 樹: 影響 B 樹高度的是每一個結點所包含的子樹數,若是儘量使結點孩子數都等於 \lceil\frac m2\rceil,則層數最多,爲最壞狀況;若是儘量使結點孩子數都等於 m,則層數最少,爲最好狀況。因此有

\log _m{(n + 1)} \leq h \leq \log _{\lceil\frac m2\rceil}{(\frac{n + 1}{2})} + 1

底數 \lceil\frac m2\rceil 能夠取很大,好比 m 能夠達到幾千,從而在關鍵字數必定的狀況下,使得最終的 h 值儘可能比較小,樹的高度比較低。

實際運用中 B 樹中的每一個結點根據實際狀況能夠包含大量的關鍵字信息和分支(但不能超過磁盤塊的大小,根據磁盤驅動的不一樣,通常塊的大小在 1k~4k 左右);這樣樹的深度下降了,意味着查找一個元素只要不多的結點從外存磁盤中讀入內存,就能夠很快地訪問到要查找的數據

查找

一個節點的結構能夠定義爲:

type BTNode struct {
  KeyNum   int       // 關鍵字個數,math.Ceil(m/2)-1 <= KeyNum < 階數 m
  Parent   *BTNode   // 指向父節點的指針
  IsLeaf   bool      // 是否爲葉子,葉子節點 children 爲 nil
  Key      []int     // 關鍵字切片,長度爲 KeyNum
  Children []*BTNode // 子節點指針切片,長度爲 KeyNum+1
}
複製代碼

以上面 2-3-4 樹的根節點爲例:

B 樹節點

 全部數據以塊的方式存儲在外磁盤中,咱們經過 B 樹來查找數據時,每遍歷到一個節點,便將其讀入內存,比較其中的關鍵字,若能匹配到咱們要找的元素,便返回;若未能找到,經過比較肯定在哪兩個關鍵字的值域區間, 便可肯定子樹的節點指針,繼續往下找,把下一個節點的數據讀入內存,重複以上步驟

插入

對於一棵 m 階的 B 樹來講,插入一個元素(或者叫關鍵字)時,首先判斷在 B 樹中是否已存在,若是存在則不插入;若是不存在,則在對應葉子結點中插入新的元素,須要判斷是否會超出關鍵字個數限制(m-1)

插入步驟:

  1. 根據元素大小查找插入位置,確定是最底層的葉子節點,將元素插入到該節點中
  2. 若是葉子節點的關鍵字個數小於等於 m-1,說明未超出限制,插入結束;不然進入下一步
  3. 若是葉子節點的關鍵字個數大於 m-1 個,以結點中間的關鍵字爲中心分裂成左右兩部分,而後將這個中間的關鍵字插入到父結點中,這個關鍵字的左子樹指向分裂後的左半部分,這個關鍵字的右子樹指向分裂後的右半部分。
  4. 而後將當前結點指向父結點,若是插入剛纔的中間關鍵字後父節點的關鍵字個數也超出限制,繼續進行第 3 步;不然結束插入

仍是以上面的 2-3-4 樹(階數 m = 4)爲例,咱們依次插入元素

  • 首先插入 一、二、3,由於關鍵字個數均未超過 m-1,因此直接插入便可:

插入 1 2 3

  • 當插入 4 時,該節點關鍵字個數達到 m,須要分裂,這裏能夠選 3 (也能夠選 2) 做爲中間字,分裂後:

插入 4

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

插入 5 7

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

插入 8

以後的步驟相似,再也不一一敘述

刪除

刪除操做是指刪除 B 樹中的某個節點中的指定關鍵字

刪除步驟:

  1. 若是當前要刪除的關鍵字位於非葉子結點 N 上,則用後繼最小關鍵字(找前繼最大關鍵字也能夠)覆蓋要刪除的關鍵字,而後在後繼關鍵字所在的子樹中刪除該後繼關鍵字。此後繼關鍵字必定位於葉子結點上,這個過程和二叉搜索樹刪除結點的方式相似。刪除這個後繼關鍵字後進入第 2 步,若是本來要刪除的關鍵字自己就位於葉子上一樣刪除關鍵字後進入第二步
  2. 該結點(假設爲 M)關鍵字個數大於等於 math.Ceil(m/2)-1,結束刪除操做,不然進入第 3 步
  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 樹爲例

2-3-4 樹

階數爲 4,節點關鍵字個數範圍應該是 [1, 3],即 math.Ceil(m/2)-1 = 1

  • 刪除關鍵字 2 或者 8,不影響節點

刪除 2 和 8

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

刪除 4

這裏第一步也能夠選擇下移 3,而後第二步跟左兄弟合併成 一、3 節點

  • 繼續刪除 1,此時與上一個問題不一樣,該葉子節點的兄弟有富餘的關鍵字,咱們只須要把父節點的臨近的一個關鍵字下移到該葉子節點代替刪除的元素,而後把兄弟節點的一個臨近關鍵字上移至父節點便可,這個操做有點相似紅黑樹的左旋操做

刪除 1

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

刪除 5

刪除操做就演示到這,B 樹的內容講完

B 樹動態展現: www.cs.usfca.edu/~galles/vis…

B+ 樹 (B+ - Tree)

B+ 樹 是基於 B 樹的變體,查找性能更好

同爲 m 階的 B+ 樹與 B 樹的不一樣點:

  1. 全部非葉子節點,每一個節點最多有 m 個關鍵字,最少有 \lceil\frac m2\rceil 個關鍵字(比 B 樹的限制多一個),其中每一個關鍵字對應一棵子樹
  2. 全部的非葉子結點能夠當作是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字,不包含關鍵字數據的指針(B 樹是包含這個指針的)
  3. 全部的葉子結點中包含了所有關鍵字的信息,及指向含這些關鍵字記錄的指針,且葉子結點自己依關鍵字的大小自小到大順序連接.(而 B 樹的所有關鍵字信息分散在各個節點中)

B+ 樹

如圖所示的是將以前的 2-3-4 樹的數據存到 B+ 樹結構中的示意圖,葉子節點保存了全部關鍵字信息而且葉子節點之間也用指針鏈接起來(一個順序鏈表),而全部非葉子節點只包含子樹根節點中對應的最大關鍵字,其做用只是用於索引

B+ 樹還能夠用另外一種形式定義:

中間節點最多有 m-1 個關鍵字,最少有 \lceil\frac m2\rceil-1 個關鍵字,與 B 樹相同; 可是非葉子節點的關鍵字是左子樹的最大關鍵字(或者右子樹的最小關鍵字),與剛纔的情形不一樣

好比一樣的數據此定義的 B+ 樹表現形式以下:

B+ 樹

這種形式中間節點佔用更少,可能更常見一點,不過下面的講解是按第一種定義來

優點

B+ 樹比 B 樹更適合實際應用中操做系統的文件索引和數據庫索引

  1. B+ 樹索引節點能夠存儲更多的關鍵字,磁盤 I/O 能夠更少

數據庫中關鍵字可能只是某個數據列的索引信息(好比以 ID 列建立的索引),而索引指向的數據記錄(某個 ID 對應的數據行)咱們稱做衛星數據,推薦看下博文 數據庫的最簡單實現MySQL索引背後的數據結構及算法原理

B- 樹中間節點和葉子節點都會帶有關鍵字和衛星數據的指針,B+ 樹中間節點只帶有關鍵字,而衛星數據的指針均放在葉子節點中

由於沒有衛星數據的指針,因此 B+ 樹內部結點相對 B 樹佔用空間更小。若是把全部同一結點的關鍵字存放在同一盤塊中,那麼對於 B+ 樹來講盤塊所能容納的關鍵字數量也就更多,一次性讀入內存中時能查找的關鍵字也就更多。相對來講 IO 讀寫次數也就下降了,性能就提高了。

舉個例子,假設磁盤中的一個盤塊能容納 16 bytes,而一個關鍵字佔 2 bytes,一個衛星數據指針佔 2bytes。對於一棵 9 階 B 樹來講,一個結點最多含 8 個關鍵字(8*4 bytes),即一個內部結點須要 2 個盤塊來存儲。而對於 B+ 樹來講,內部結點不含衛星數據的指針,因此一個內部節點只須要 1 個盤塊。當須要把內部結點讀入內存中的時候,B 樹就比 B+ 樹多一次盤塊查找時間

  1. B+ 樹的查詢效率更加穩定

因爲非葉子節點並非最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。因此任何關鍵字的查找必須走一條從根結點到葉子結點的路徑。全部關鍵字查詢的路徑長度相同,致使每個數據的查詢效率至關。而 B 樹查找一個文件時查找到的路徑長度是不一的。

  1. B+ 樹對範圍查詢操做更友好

若是是查找單一元素,B+ 樹的查找過程與 B 樹相似,只是每次查找都是從根查到葉

而進行範圍查詢的操做時,B+ 樹只要遍歷葉子節點就能夠實現整棵樹的遍歷,而 B 樹的範圍查詢要經過中序遍歷,效率比較低下

插入

B+ 樹的插入與 B 樹相似,先尋找關鍵字對應的位置插入,須要注意的是插入比當前子樹的最大關鍵字還大的數時要修改祖先節點對應的關鍵字,由於 B+ 樹內部結點存的是子樹的最大關鍵字

好比在上面給出的 B+ 樹中插入 105 這個元素,由於 105 大於當前子樹最大關鍵字 101,因此須要修改父節點和祖父節點的邊界關鍵字:

插入 105

  • 若是插入元素的節點未超出上界限制,則結束;不然將節點分裂,中間節點上移到父節點中,再判斷父節點是否須要調整

好比剛纔插入 105 的葉子節點關鍵字個數達到 4 個,須要分裂,這裏分裂與 B 樹略有不一樣。B 樹是把節點按中間節點分紅三份,再把中間節點上移;而 B+ 樹是分紅兩份,再把左半節點的最大關鍵字添加進父節點

分裂葉子

此時父節點也須要分裂

分裂父節點

根節點未超出 4,結束;假如此時根節點也超出上界了,須要把根節點也分裂,生成一個新的根節點,且新的根節點的關鍵字爲左右子樹的最大關鍵字

刪除

B+ 樹的刪除與 B 樹也相似,找到要刪除的關鍵字,若是是當前子樹的最大關鍵字,刪除該關鍵字後還要修改祖先節點對應的關鍵字;若是不是當前子樹的最大關鍵字,直接刪除;

在上一張圖的基礎上刪除 8,這是葉子的最大關鍵字,因此須要修改父節點和祖父節點的邊界關鍵字:

刪除 8

  • 若是刪除元素的節點未低於下界限制,則結束;不然分兩種狀況處理:
    • 若是兄弟節點有富餘關鍵字,則從兄弟節點中移動一個關鍵字到當前節點,修改父節點對應邊界關鍵字便可
    • 若是兄弟節點關鍵字個數都處於下界值,不能外借元素,則合併當前節點和兄弟節點,修改父節點的孩子指針以及邊界關鍵字,此時父節點關鍵字個數也少了一個,將當前節點的指針指向父節點繼續判斷處理

咱們繼續刪除 7,此時該葉子節點關鍵字個數少於 1 須要調整,而兄弟節點有富餘關鍵字,能夠移動 5 到當前節點,修改父節點和祖父節點的邊界關鍵字

刪除 7

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

刪除 5

B+ 樹動態展現: www.cs.usfca.edu/~galles/vis…

B* 樹 (B* - Tree)

B* 樹是 B+ 樹的變體,在 B+ 樹的基礎上(全部的葉子結點中包含了所有關鍵字的信息,及指向含有這些關鍵字記錄的指針),B* 樹多了兩條性質:

  • 中間結點也增長了指向兄弟的指針,即每一層節點均可以橫向遍歷
  • B* 樹定義了非葉子結點關鍵字個數至少爲 \lceil\frac {2m}3\rceil,即塊的最低使用率爲 2/3,代替 B+ 樹的 1/2

下圖的數據與以前 B+ 樹的數據同樣,但分支結構有所不一樣(由於中間節點關鍵字範圍變爲[3, 4],不一樣於以前 B+ 樹的 [2, 4]),並且第二層節點之間也用指針鏈接起來

B* 樹

優點

B+ 樹節點滿時就會分裂,而 B* 樹節點滿時會先檢查兄弟節點是否滿(由於每一個節點都有指向兄弟的指針):

  • 若是兄弟節點未滿則向兄弟節點轉移關鍵字,而後修改原節點和兄弟結點的關鍵字以及會受最大關鍵字變更影響的祖先的邊界關鍵字
  • 若是兄弟節點已滿,則從當前節點和兄弟節點各拿出 1/3 的數據建立一個新的節點出來,而後在父結點增長新結點的指針

B* 樹存有兄弟節點的指針,能夠向兄弟節點轉移關鍵字的特性使得 B* 樹分解次數變得更少,節點空間使用率更高

由於沒有找到相關的內容,關於 B* 樹的插入刪除這裏再也不講解

總結

本文依次介紹了二叉樹 -> 二叉搜索樹 -> 平衡二叉搜索樹(紅黑樹) -> 平衡多路查找樹(B 類樹),各有特色,其中 B 類樹是介紹的重點,由於實際運用中索引結構使用的是 B 類樹

由於樹的上面幾層會反覆查詢,因此咱們能夠把樹的前幾層存在內存中,而底層的數據存在外部磁盤裏,這樣效率更高

固然 B 樹也存在弊端:

由於一旦肯定最大階數,後面的使用過程當中就不能夠修改關鍵字個數的範圍

那麼除非徹底重建數據庫,不然沒法改變鍵值的最大長度。這使得許多數據庫系統將人名截斷到 70 字符以內

後面一篇咱們會講解另外一種 Mysql 的索引結構: 哈希索引,能夠動態適應任意長度的鍵值

Reference

相關文章
相關標籤/搜索