原文連接: blog.wangriyu.wang/2018/06-Tre…html
與數據庫相關的樹結構主要爲 B 類樹,B 類樹一般用於數據庫和操做系統的文件系統mysql
在學習 B 類樹以前先複習一下二叉查找樹的概念和紅黑樹golang
二叉樹 - Binary Tree 是每一個節點最多隻有兩個分支(即不存在分支度大於 2 的節點)的樹結構。面試
二叉查找樹 - Binary Search Tree: 也稱二叉搜索樹、有序二叉樹。對於根樹和全部子樹都知足,每一個節點都大於左子樹元素,而小於右子樹元素,且沒有鍵值相等的結點算法
搜索、插入、刪除的複雜度等於樹高,指望 ,最壞 O(n)(數列有序,樹退化成線性表)sql
二叉查找樹動態展現: visualgo.net/zh/bst數據庫
當數據基本有序時,二叉查找樹會退化成線性表,查找效率嚴重降低編程
因此後面出現了不少改進的平衡樹結構以知足樹高最壞也爲 , 如伸展樹 (Splay Tree)、平衡二叉樹 (SBT)、AVL 樹、紅黑樹等數據結構
紅黑樹 - Red–black tree 是一種自平衡二叉查找樹,除了符合二叉查找樹的性質外,它還知足如下五條性質:性能
上述約束確保了紅黑樹的關鍵特性: 從根到葉子的最長路徑不會超過最短路徑的兩倍
證實: 主要看性質 4 和 性質 5,假設從根到葉子的最短路徑 a 上有黑色節點 n 個,最長路徑 b 確定是交替的紅色和黑色節點,而根據性質 5 可知從根到葉子的全部路徑都有相同數目的黑色節點, 這就代表 b 的黑色節點也爲 n 個,但 b 出現的紅色節點不可能超過黑色節點個數,不然會破壞性質 4 (抽屜原理),因此從根到葉子的最長路徑不會超過最短路徑的兩倍
由於每個紅黑樹也是一個特化的二叉查找樹,所以紅黑樹上的只讀操做與普通二叉查找樹上的只讀操做相同。然而,在紅黑樹上進行插入操做和刪除操做會致使再也不匹配紅黑樹的性質。 恢復紅黑樹的性質須要少許 的顏色變動(實際是很是快速的)和不超過三次樹旋轉(對於插入操做是兩次)。雖然插入和刪除很複雜,但操做時間仍能夠保持爲
次。
紅黑樹發生變動時須要 [變色] 和 [旋轉] 來調整,其中旋轉又分 [左旋] 和 [右旋]。
逆時針
旋轉紅黑樹的兩個節點 X-Y,使得父節點被本身的右孩子取代,而本身降低爲左孩子順時針
旋轉紅黑樹的兩個節點 X-Y,使得父節點被本身的左孩子取代,而本身降低爲右孩子旋轉過程當中只須要作三次指針變動就行
插入節點的位置跟二叉查找樹的尋找方法基本一致,若是插入結點 z 小於當前遍歷到的結點,則到當前結點的左子樹中繼續查找,若是 z 大於當前結點,則到當前結點的右子樹中繼續查找, 若是 z 依然比此刻遍歷到的新的當前結點小,則 z 做爲當前結點的左孩子,不然做爲當前結點的右孩子。而紅黑樹插入節點後,爲了保持約束還須要進行調整修復(變色加旋轉)。
因此插入步驟以下: 紅黑樹按二叉查找樹的規則找到位置後插入新節點 z,z 的左孩子、右孩子都是葉子結點 nil, z 結點初始都爲紅色,再根據下述情形進行變色旋轉等操做,最後達到平衡。
好比上圖插入 12 時知足情形 2:
如下情形須要做出額外調整:
下面着重講講後三種狀況如何調整
當前結點的父結點是紅色且祖父結點的另外一個子結點(叔叔結點)是紅色
由於當前節點的父節點是紅色,因此父節點不多是根節點,當前節點確定有祖父節點,也就有叔叔節點
解決步驟: 將當前結點的父結點和叔叔結點塗黑,祖父結點塗紅,再把祖父結點當作新節點(即當前節點的指針指向祖父節點)從新檢查各類情形進行調整
因爲對稱性,無論父結點是祖父結點的左子仍是右子,當前結點是其父結點的左子仍是右子,處理都是同樣的
咱們插入 21 這個元素,當前節點指向 21:
此時會發現 2一、22 兩個紅色相連與性質 4 衝突,但 21 節點知足情形 3,修復後:
此時當前節點指向 21 的祖父節點,即 25。而 25 節點一樣遇到情形 3 的問題,繼續修復:
此時當前節點指向根節點,知足情形 1,將 14 節點塗黑便可恢復紅黑樹平衡
當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置不在同側
解決步驟:
在上圖的基礎上咱們繼續插入 5 這個元素:
能夠看出 5 是父節點的左子,而父節點是祖父節點的右子,不一樣側則爲情形 4,將當前節點指向 5 的父節點 6,並以 6 爲支點進行右旋:
此時當前節點是 6,而 6 是父節點 5 的右子,父節點 5 也是祖父節點 1 的右子,同側則轉爲情形 5,繼續往下看
當前結點的父結點是紅色,叔叔結點是黑色或者 nil,當前結點相對其父結點的位置和父節點相對祖父節點的位置在同側
解決步驟:
在上一張圖的基礎上修改節點 5 爲黑色,節點 1 爲紅色,再以 1 爲支點左旋:
此時便恢復平衡
刪除節點 X 時第一步先判斷兩個孩子是否都是非空的,若是都非空,就先按二叉查找樹的規則處理:
在刪除帶有兩個非空子樹的節點 X 的時候,咱們能夠找到左子樹中的最大元素(或者右子樹中的最小元素),並把這個最值複製給 X 節點,只代替原來要刪除的值,不改變節點顏色。
而後咱們只要刪除那個被複製出值的那個節點就行,由於是最值節點因此它的孩子不可能都非空。
由於只是複製了一個值,不違反任何性質,這就把原問題轉化爲如何刪除最多有一個非空子樹的節點的問題。它不關心這個節點是最初要刪除的節點仍是被複製出值的那個節點。
咱們以圖爲例,圖中三角形表明可能爲空的子樹:
節點 X 是要刪除的節點,發現它的兩個子樹非空,咱們能夠找左子樹中最大的元素 Max (也能夠找右子樹中最小的元素 Min),把 Max 值(或者 Min 值)複製到 X 上覆蓋原來的值,不修改其餘屬性,而後刪除 Max 節點(或 Min 節點)便可,能夠很清楚的看到最值節點最多隻會有一個非空子樹
接下來就是如何處理刪除最多有一個非空子樹的節點 X 的問題
簡單情形:
若是 X 和它的兒子都是黑色,這是一種複雜的狀況,咱們單拎出來說
咱們首先把要刪除的節點 X 替換爲它的兒子。出於方便,稱呼這個新上位的兒子爲 N,稱呼它的兄弟爲 S,使用 P 稱呼 N 的新父親,SL 稱呼 S 的左兒子,SR 稱呼 S 的右兒子
有如下六種情形須要考慮:
N 是新的根
咱們不須要作什麼,由於全部路徑都去除了一個黑色節點,而新根也是黑色的,因此性質都保持着
情形 二、五、6 涉及到左右不一樣的狀況,只取一種處理
S 是紅色
完成這兩個操做後,儘管全部路徑上黑色節點的數目沒有改變,但如今 N 有了一個黑色的兄弟和一個紅色的父親,因此咱們能夠接下去按情形 四、情形 5 或情形 6 來處理
N 的父親、S 和 S 的兒子都是黑色的
在這種情形下,咱們簡單的重繪 S 爲紅色。結果是經過 S 的全部路徑都少了一個黑色節點。這與刪除 N 的初始父親 X 形成經過 N 的全部路徑少了一個黑色節點達成平衡。可是,經過 P 的全部路徑如今比不經過 P 的路徑少了一個黑色節點,因此仍然違反性質 5。要修正這個問題,咱們要從情形 1 開始,在 P 上作從新平衡處理
S 和 S 的兒子都是黑色,可是 N 的父親是紅色
在這種情形下,咱們簡單的交換 N 的兄弟和父親的顏色。這不影響不經過 N 的路徑的黑色節點的數目,可是它在經過 N 的路徑上對黑色節點數目增長了一,添補了在這些路徑上刪除的黑色節點
S 是黑色,S 的其中一個兒子是紅色,且紅色兒子的位置與 N 相對於父親的位置處於同側
全部路徑仍有一樣數目的黑色節點,可是如今 N 有了一個黑色兄弟,且兄弟的一個兒子仍爲紅色的,其位置與 N 相對於父親的位置處於不一樣側,進入情形 6
情形 五、6 中父節點 P 的顏色能夠爲黑色也能夠是紅色
S 是黑色,S 的其中一個兒子是紅色,且其位置與 N 相對於父親的位置處於不一樣側
交換前 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:
此時 N 的兄弟 S 變爲 22,P 變爲 17,S 的左孩子是紅色的 21,屬於情形 5。S 作右旋轉,並交換 22 和 21 的顏色:
此時 N 的兄弟 S 變爲黑色的 21,但 21 的紅色孩子節點 22 變爲右側,進入情形 6
P 節點 17 作左旋轉,並將 S 的右節點塗黑,此時樹恢復平衡
至此,咱們已經把節點都刪了個遍,相信你對紅黑樹的刪除操做應該瞭解了
紅黑樹動態展現: www.cs.usfca.edu/~galles/vis…
紅黑樹仍是典型的二叉搜索樹結構,主要應用在一些 map 和 set 類型的實現上,好比 Java 中的 TreeMap 和 C++ 的 set/map/multimap 等。其查找的時間複雜度 與樹的深度相關,下降樹的深度能夠提升查找效率。
Java 的 hashmap 和 golang 的 map 是用哈希實現的
可是大規模數據存儲中,實現索引查詢這樣一個實際背景下,樹節點存儲的元素數量是有限的(若是元素數量很是多的話,查找就退化成節點內部的線性查找了), 這樣致使二叉查找樹結構因爲樹的深度過大而形成磁盤 I/O 讀寫過於頻繁,進而致使查詢效率低下,所以咱們該想辦法下降樹的深度,從而減小磁盤查找存取的次數。
一個基本的思想就是:採用多叉樹結構
,因此出現了下述的平衡多路查找樹
B-樹,即爲 B 樹,不要讀做 B 減樹
B 樹與紅黑樹最大的不一樣在於,B 樹的結點能夠有許多子女,從幾個到幾千個。
B 樹的定義有兩種,一種以階數爲限制的 B 樹(下文所述的),一種以度數爲限制的 B 樹(算法導論所描述的),二者原理相似,這裏以階數來定義
B 樹屬於平衡多路查找樹。一棵 m 階(m 階即表明樹中任一結點最多含有 m 個孩子)的 B 樹的特性以下:
如圖是一個典型的 2-3-4 樹結構,也是階爲 4 的 B 樹。從圖中查詢元素最多隻須要 3 次磁盤 I/O 就能夠訪問到咱們須要的數據節點,將節點數據塊讀入內存後再查找指定元素會很快。若是一樣的數據用紅黑樹表示,樹高會增加不少,形成遍歷節點的次數增多,訪問磁盤的次數增多,查找性能會降低。
對於一棵包含 n 個元素、高度爲 h 、階數爲 m 的 B 樹: 影響 B 樹高度的是每一個結點所包含的子樹數,若是儘量使結點孩子數都等於 ,則層數最多,爲最壞狀況;若是儘量使結點孩子數都等於 m,則層數最少,爲最好狀況。因此有
底數 能夠取很大,好比 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 樹來查找數據時,每遍歷到一個節點,便將其讀入內存,比較其中的關鍵字,若能匹配到咱們要找的元素,便返回;若未能找到,經過比較肯定在哪兩個關鍵字的值域區間, 便可肯定子樹的節點指針,繼續往下找,把下一個節點的數據讀入內存,重複以上步驟
對於一棵 m 階的 B 樹來講,插入一個元素(或者叫關鍵字)時,首先判斷在 B 樹中是否已存在,若是存在則不插入;若是不存在,則在對應葉子結點中插入新的元素,須要判斷是否會超出關鍵字個數限制(m-1)
插入步驟:
仍是以上面的 2-3-4 樹(階數 m = 4)爲例,咱們依次插入元素
以後的步驟相似,再也不一一敘述
刪除操做是指刪除 B 樹中的某個節點中的指定關鍵字
刪除步驟:
以上面的 2-3-4 樹爲例
階數爲 4,節點關鍵字個數範圍應該是 [1, 3],即 math.Ceil(m/2)-1 = 1
這裏第一步也能夠選擇下移 3,而後第二步跟左兄弟合併成 一、3 節點
刪除操做就演示到這,B 樹的內容講完
B 樹動態展現: www.cs.usfca.edu/~galles/vis…
B+ 樹 是基於 B 樹的變體,查找性能更好
同爲 m 階的 B+ 樹與 B 樹的不一樣點:
如圖所示的是將以前的 2-3-4 樹的數據存到 B+ 樹結構中的示意圖,葉子節點保存了全部關鍵字信息而且葉子節點之間也用指針鏈接起來(一個順序鏈表),而全部非葉子節點只包含子樹根節點中對應的最大關鍵字,其做用只是用於索引
B+ 樹還能夠用另外一種形式定義:
中間節點最多有 m-1 個關鍵字,最少有
個關鍵字,與 B 樹相同; 可是非葉子節點的關鍵字是左子樹的最大關鍵字(或者右子樹的最小關鍵字),與剛纔的情形不一樣
好比一樣的數據此定義的 B+ 樹表現形式以下:
這種形式中間節點佔用更少,可能更常見一點,不過下面的講解是按第一種定義來
B+ 樹比 B 樹更適合實際應用中操做系統的文件索引和數據庫索引
數據庫中關鍵字可能只是某個數據列的索引信息(好比以 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+ 樹多一次盤塊查找時間
因爲非葉子節點並非最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。因此任何關鍵字的查找必須走一條從根結點到葉子結點的路徑。全部關鍵字查詢的路徑長度相同,致使每個數據的查詢效率至關。而 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+ 樹的變體,在 B+ 樹的基礎上(全部的葉子結點中包含了所有關鍵字的信息,及指向含有這些關鍵字記錄的指針),B* 樹多了兩條性質:
下圖的數據與以前 B+ 樹的數據同樣,但分支結構有所不一樣(由於中間節點關鍵字範圍變爲[3, 4],不一樣於以前 B+ 樹的 [2, 4]),並且第二層節點之間也用指針鏈接起來
B+ 樹節點滿時就會分裂,而 B* 樹節點滿時會先檢查兄弟節點是否滿(由於每一個節點都有指向兄弟的指針):
B* 樹存有兄弟節點的指針,能夠向兄弟節點轉移關鍵字的特性使得 B* 樹分解次數變得更少,節點空間使用率更高
由於沒有找到相關的內容,關於 B* 樹的插入刪除這裏再也不講解
本文依次介紹了二叉樹 -> 二叉搜索樹 -> 平衡二叉搜索樹(紅黑樹) -> 平衡多路查找樹(B 類樹),各有特色,其中 B 類樹是介紹的重點,由於實際運用中索引結構使用的是 B 類樹
由於樹的上面幾層會反覆查詢,因此咱們能夠把樹的前幾層存在內存中,而底層的數據存在外部磁盤裏,這樣效率更高
固然 B 樹也存在弊端:
由於一旦肯定最大階數,後面的使用過程當中就不能夠修改關鍵字個數的範圍
那麼除非徹底重建數據庫,不然沒法改變鍵值的最大長度。這使得許多數據庫系統將人名截斷到 70 字符以內
後面一篇咱們會講解另外一種 Mysql 的索引結構: 哈希索引,能夠動態適應任意長度的鍵值