B樹、B+樹詳解
B樹
前言
首先,為什么要總結B樹、B+樹的知識呢?最近在學習數據庫索引調優相關知識,數據庫系統普遍采用B樹、B+樹作為索引結構,例如 MYSQL的InnoDB引擎使用的就是B+樹,理解不透徹B樹,則無法理解數據庫的索引機制,接下倆將用最簡潔直白的內容來了解B樹、B+樹的數據結構。
另外,B-樹,即為B樹,因為B樹的原英文名稱為B-TREE,而國內很多人喜歡B-Tree譯作B-樹,其實,這是個很不好的直譯,很容易讓人產生誤解。如人們可能會以為B-樹是一種樹,而B-樹又是一種樹,而事實上,B-Tree就是指的是B樹,目前B的意思理解為平衡。
B樹的出現是為了彌合不同的存儲級別之間的訪問速度上的巨大差異,實現高效的I/O,平衡二叉樹的查找效率是非常高的,這樣會導致二叉查找樹結構由於樹的深度過大而造成磁盤I/O讀寫過於頻繁,進而導致查詢效率低下,另外數據量過大會導致內存空間不夠容納平衡二叉樹所有節點的情況,B樹是解決這個問題的很好的結構。
概念
首先,B樹不要和二叉樹混淆,在計算機科學中,B樹是一種自平衡數據結構,它維護有序數據並允許以對數時間進行搜索,順序訪問,插入和刪除。B數是二叉搜索樹的一般化,因為節點可以有兩個以上的子節點,這與其他自平衡二進制搜索樹不同,B樹非常適合讀取和寫入相對較大的數據塊(如光盤)的存儲系統。它通常用於數據庫和文件系統。
定義
B樹是一種平衡的多分樹,通常我們所說m階的B樹,它必須滿足以下條件:
- 每個節點最多只有m個子節點
- 每個非葉子節點(除了根)具有至少【m/2】個子節點
- 如果根不是葉子節點,則根至少有兩個子節點
- 具有k個子節點的非葉子節點包含k-1個鍵
- 所有葉子節點都出現在同一水平,沒有任何信息(高度一致)
什么是B樹的階?
B樹中的一個節點的子節點數目的最大值,用m表示,假如最大值為10,則為10階,如圖:
所有節點中,節點【13,16,19】擁有的子節點數目最多,四個子節點(灰色節點),所以可以定義為上面的樹為4階B樹。
什么是根節點?
節點【10】即為根節點,特征:根節點擁有的子節點的數量的上限和內部節點相同,如果根節點不是樹中唯一節點的話,至少有兩個子節點(不然就變成單支了)。在m階B樹中(根節點非樹中唯一節點),那么有關系式2<=M<=m,M為子節點數量;包含的元素數量1<=K<=m-1,K為元素數量。(其中,元素數量為每一個節點中的所包含的數的數量)。
什么是內部節點?
節點【13,16,19】、節點【3,6】都為內部節點,特征:內部節點是除葉子節點之外的所有節點,擁有父節點和子節點。假定m階B樹的內部節點的子節點的數量為M,則一定要符合(m/2)<=M<=m的關系式,包含元素數量M-1;包含的元素數(m/2)-1<=K<=m-1,K為元素數量。m/2向上取整。
什么是葉子節點?
節點【1,2】、節點【11,12】等最后一層都為葉子節點,葉子節點對元素數量有相同的限制,但是沒有子節點,也沒有指向子節點的指針。特征:在m階B樹種葉子節點的元素符合(m/2)-1<=K<=m-1
插入
針對m階高度為h的B樹,插入一個元素的時候,首先在B樹中是否存在,如果不存在,即在葉子節點處結束,然后再葉子節點中插入該新的元素。
- 若該節點元素個數小於m-1,直接插入。
- 若該節點元素個數等於m-1,引起節點分裂;以該節點中間元素為分界,取中間元素(偶數個數,中間兩個隨機選取)插入到父節點中;
- 重復上面的動作,直到所有節點符合B樹的規則;最壞的情況一直分裂到根節點,生成新的根節點,高度增加1;
上面三段話為插入動作的核心,接下來以5階B樹為例,詳細講解插入的動作:
5階B樹的關鍵點:
- 2<=根節點的子節點個數<=5
- 3<=內節點子節點個數<=5
- 1<=根節點元素個數<=4
- 2<=內部節點元素個數<=4
圖(1)插入元素【8】后變為圖(2),此時根節點的元素個數為5,不符合1<=根節點元素個數<=4,進行分裂,(真實情況是先分裂,然后插入元素,這里是為了直觀而先插入元素,下面的操作都一樣,不再贅述),取節點中間元素【7】,加入到父節點,左右分裂為兩個節點,插入是在葉子節點處進行插入的,如圖(3):
接着插入元素【5】,【11】,【17】的時候,不需要任何分裂操作,如圖【4】:
插入元素【13】
這時,節點數量超出最大數量,進行分裂,提取中間元素【13】,插入到父節點當中,如圖6:
接着插入元素【6】,【12】,【20】,【23】的時候,不需要任何分裂操作,如圖7:
插入26的時候,最右邊的葉子節點滿了,需要進行分裂操作,中間元素【20】上移到父節點中,注意通過上移中間元素,樹最終還是保持平衡,分裂結果的節點存在兩個關鍵字元素。
插入【4】的時候,導致最左邊的葉子節點被分裂,【4】恰好也是中間元素,上移到父節點中,然后元素【16】,【18】,【24】,【25】陸續插入不需要任何分裂操作
最后,當插入【19】的時候,含有【14】,【16】,【17】,【18】的節點需要分裂,把中間元素【17】上移到父節點中,但是情況來了,父節點中的空間已經滿了,所以也要進行分裂,將父節點中的中間元素【13】上移到新形成的根節點中,這樣具體插入操作的完成。
刪除
首先查找B樹種需要刪除的元素,如果該元素在B樹種存在,則將該元素在其節點種進行刪除;刪除該元素后,首先判斷該元素是否有左右孩子節點,如果有,則上移孩紙節點種的某相近元素(左孩紙最右邊的節點或者右孩子最左邊的節點)到父節點種,然后是移動之后的情況;如果沒有,直接刪除。
- 某節點中元素數目小於(m/2)-1,(m/2)向上取整,則需要看其某相鄰兄弟節點是否豐滿
- 如果豐滿(節點中元素個數大於(m/2)-1),則向父節點借一個元素來滿足條件
- 如果其相鄰兄弟的都不豐滿,則其節點數目等於(m/2)-1,則該節點與其相鄰的某一兄弟節點進行合並成一個節點。
接下來還以5階B樹為例子,詳細講解刪除的動作:
關鍵要領:元素個數小於2(m/2-1)就合並,大於4(m-1)就分裂
如圖依次刪除【8】,【20】,【18】,【5】
首先刪除元素【8】,當然先查找【8】,【8】在一個葉子節點中,刪除后該葉子節點元素個數為2,符合B樹規則,操作很簡單,咱們只需要移動【11】至原來【8】的位置,移動【12】到【11】的位置(也就是節點中刪除元素后面的元素向前移動)
下一步,刪除【20】,因為【20】沒有在葉子節點中,而是在中間節點中找到,咱們發現他的繼承者【23】(字母升序的下個元素),將【23】上移到【20】的位置,然后將孩子節點中的【23】進行刪除,這里恰好刪除后,該孩紙節點中元素個數大於2,無需進行合並操作。
下一步刪除【18】,【18】在葉子節點中,但是該節點中元素數目為2,刪除導致只有1個元素,已經小於最小元素數目為2,而由前面我們已經知道:如果其相鄰某個兄弟節點中比較豐滿,(元素個數大於ceil(5/2)-1=2),則可以向父節點借一個元素,然后將最豐滿的相鄰兄弟節點中上移最后或者最前一個元素到父節點中,在這個實例中,右相鄰兄弟節點中比較豐滿(3個元素大於2),所以先向父節點借一個元素【23】下移到該葉子節點中,代替原來【19】的位置,【19】前移,然后【24】在相鄰兄弟節點中上移到父節點中,最后在相鄰兄弟節點中刪除【24】,后面元素前移。
最后一步刪除【5】,刪除后會導致很多問題,因為【5】所在的節點數目剛好全部達標,剛好滿足最小元素個數(ceil(5/2)-1=2),而相鄰的兄弟節點也是同樣的情況,刪除一個元素都不能滿足條件,所以需要該節點與某相鄰兄弟節點進行合並操作;首先移動父節點中的元素(該元素再兩個需要合並的兩個節點元素之間)下移動到其子節點中,然后將1兩個節點進行合並成一個節點。所以再改實例中,咱們首先將父節點的元素【4】下移動到已經刪除【5】而只有【6】的節點中,然后將含有【4】和【6】的節點和含有【1】,【3】的相鄰兄弟節點進行合並成一個節點。
也許你認為這樣刪除操作已經結束了,其實不然,再看看上圖,對於這種特殊情況,你會立即發現父節點只包含一個元素【7】,沒達標(因為非根節點包括葉子節點的元素K必須滿足於2<=K<=4,此處的K=1),這時不能夠接受的。如果這個問題節點的相鄰兄弟元素比較豐滿,可以向父元素借一個元素。而此時兄弟節點元素剛好為2,剛剛滿足,只能進行合並,而根節點中的唯一元素【13】下移到子節點,這樣的高度減少一層。
磁盤I/O與預讀
計算機存儲設備一般分為兩種:內存儲器和外存儲器器
內存儲器為內存,內存存取速度快,但容量小,價格昂貴,而且不能長期保存數據(在不通電情況下數據會消失)。
外存儲器即為磁盤讀取,磁盤讀取數據靠的是機械運動,每次讀取數據花費的時間可以分為尋道時間、旋轉延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁道所需要的時間,主流磁盤一般在5ms以下;旋轉延遲就是我們經常聽說的磁盤轉速,比如一個磁盤7200轉,表示每分鍾能轉7200次,也就是說1秒鍾能轉120次,旋轉延遲就是1/120/2 = 4.17ms;傳輸時間指的是從磁盤讀出或將數據寫入磁盤的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計。那么訪問一次磁盤的時間,即一次磁盤IO的時間約等於5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一台500 -MIPS的機器每秒可以執行5億條指令,因為指令依靠的是電的性質,換句話說執行一次IO的時間可以執行40萬條指令,數據庫動輒十萬百萬乃至千萬級數據,每次9毫秒的時間,顯然是個災難。
考慮到磁盤I/O是非常高昂的操作,計算機操作系統做了一些優化,當一次I/O操作的時候,不光把當前磁盤地址的數據,而是把相鄰的數據也讀到內存緩沖區內,因為局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,其相鄰的數據也很快被訪問到。每一次IO讀取的數據我們稱之為第一頁,具體第一頁有多大的數據跟操作系統有關,一般為4k或者8k,也就是我們讀取一頁內容的時候,實際上才發生了一次IO操作,這個理論對於索引的數據結構設計非常有幫助。
事實1 : 不同容量的存儲器,訪問速度差異懸殊。
- 磁盤(ms級別) << 內存(ns級別), 100000倍
- 若內存訪問需要1s,則一次外存訪問需要一天
- 為了避免1次外存訪問,寧願訪問內存100次...所以將
最常用
的數據存儲在最快的存儲器中
事實2:從磁盤1中讀取1B,與讀寫1KB的時間成本幾乎一樣。
從以上數據可以總結出一個道理,索引查詢的數據主要受限於硬盤的IO速度,查詢IO次數越少,速度越快,所以B樹的結構才會應需求而生;B樹的每個節點可以視為一次IO讀取,樹的高度表示最多的IO次數,在相同數量的總元素個數下,每個節點的元素個數越多,高度越低,查詢所用的IO次數越少,假設,一次硬盤一次I/O數據為8K,索引用int(4字節)類型數據建立,理論上一個節點最多可以為2000個元素,200020002000=8000000000,80億條的數據只需3次I/O(理論值),可想而知,B樹做為索引的查詢效率有多高;
另外也可以看出同樣的總元素個數,查詢效率和樹的高度密切相關
B樹的高度
一棵樹含有N個總關鍵字樹的m階的B樹的最大高度是多少?
log(m/2)(N+1)+1 log以(m/2)為底,(N+1)/2的對數再加1
算法如下:
B+樹
- 有m個子樹的中間節點包含有m個元素(B樹種是K-1個元素),每個元素不保存數據,只用來索引
- 所有的葉子節點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針,且葉子節點本身依依關鍵字的大小自小二大的順序鏈接。(而B樹的葉子節點並沒有包含全部要查找的信息)。
- 所有的非終端節點可以看成是索引部分,節點中僅含有其子樹根節點中最大(或者最小)關鍵字。(而B樹的非終端節點包含需要查找的有效信息);
為什么說B+樹比B樹更適合做數據庫索引?
-
B+樹的磁盤讀寫代價更低
B+樹的內部節點並沒有指向關鍵字具體信息的指針。因此其內部節點相對B樹更小。如果把同一內部節點的關鍵字放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。一次性讀取內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
-
B+樹查詢效率更穩定
由於非終端節點並不是最終指向文件內容的節點,而只是葉子節點關鍵字的索引。索引任何關鍵字的查找必須走一條根節點到葉子節點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。
-
B+樹便於范圍查詢(最重要的原因,范圍查找是數據庫的常態)
B樹再提高了IO性能的同時並沒有解決掉元素遍歷低下的問題,正是為了解決這個問題,B+樹應用而出,B+樹只需要去遍歷葉子節點就可以實現整棵樹的遍歷。而且再數據庫中基於范圍的查詢是非常頻繁的,而B樹不支持這樣的操作或者說效率太低。
不懂可以看看這篇解讀-》范圍查找
補充:B樹的范圍查找用的是中序遍歷,而B+樹用的是再鏈表上遍歷