B樹、B+樹、B*樹

B-樹,就是B樹,B樹的原英文名是B-tree,所以很多翻譯為B-樹,就會很多人誤以為B-樹是一種樹、B樹是另外一種樹。其實,B-tree就是B樹。

B樹是一種多叉平衡查找樹,我們之前所介紹的紅黑樹是二叉查找樹結構,B樹由于是多叉結構,對于元素數量非常多的情況下,樹的深度不會像二叉結構那么大,可以保證查詢效率。

B樹的性質(m階的B樹)

  1. 樹中每個結點最多含有m個孩子(m>=2);
  2. 除根結點和葉子結點外,其它每個結點至少有[ceil(m / 2)]個孩子(其中ceil(x)是一個取上限的函數);
  3. 根結點至少有2個孩子(除非B樹只包含一個結點:根結點);
  4. 所有葉子結點都出現在同一層,葉子結點不包含任何關鍵字信息(可以看做是外部結點或查詢失敗的結點,指向這些結點的指針都為null);(注:葉子節點只是沒有孩子和指向孩子的指針,這些節點也存在,也有元素。類似紅黑樹中,每一個NULL指針即當做葉子結點,只是沒畫出來而已)。
  5. 每個非終端結點中包含有n個關鍵字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
    a) Ki (i=1...n)為關鍵字,且關鍵字按順序升序排序K(i-1)< Ki。
    b) Pi為指向子樹根的結點,且指針P(i-1)指向子樹種所有結點的關鍵字均小于Ki,但都大于K(i-1)。
    c) 關鍵字的個數n必須滿足: [ceil(m / 2)-1]<= n <= m-1。比如有j個孩子的非葉結點恰好有j-1個關鍵碼。

B樹的插入
根據B樹的性質,一個m階的B樹需要滿足:

  • 樹中每個結點含有最多含有m個孩子,即m滿足:ceil(m/2)<=m<=m。
  • 除根結點和葉子結點外,其它每個結點至少有[ceil(m / 2)]個孩子(其中ceil(x)是一個取上限的函數);
  • 除根結點之外的結點的關鍵字的個數n必須滿足: [ceil(m / 2)-1]<= n <= m-1(葉子結點也必須滿足此條關于關鍵字數的性質)。

針對一棵高度為h的m階B樹,插入一個元素時,首先在B樹中是否存在,如果不存在,一般在葉子結點中插入該新的元素,此時分3種情況:

  • 如果葉子結點空間足夠,即該結點的關鍵字數小于m-1,則直接插入在葉子結點的左邊或右邊;

  • 如果空間滿了以致沒有足夠的空間去添加新的元素,即該結點的關鍵字數已經有了m個,則需要將該結點進行“分裂”,將一半數量的關鍵字元素分裂到新的其相鄰右結點中,中間關鍵字元素上移到父結點中,而且當結點中關鍵元素向右移動了,相關的指針也需要向右移。

  • 此外,如果在上述中間關鍵字上移到父結點的過程中,導致根結點空間滿了,那么根結點也要進行分裂操作,這樣原來的根結點中的中間關鍵字元素向上移動到新的根結點中,因此導致樹的高度增加一層。

插入以下字符字母到一棵空的5階B 樹中:C N G A H E K Q M F W L T Z D P R X Y S
分析: 根據上面的性質總結,5階的B樹,非根節點關鍵字個數n滿足2<=n<=4,每個節點最多含有5個孩子,除根節點葉子節點之外,其他節點至少3個孩子。

  1. 關鍵字個數最大4,先取前4個插入到相同的節點中。


    1.jpg
  2. 插入H,因為步驟一后空間不夠,就需要將中間關鍵字元素上移到父結點中,樹增加一層


    2.jpg
  3. 在步驟二的圖中,可以繼續插入E,K,Q三個節點,繼續插就得分裂


    3.jpg
  4. 插入M將進行分裂,M剛好是中間元素,直接上移到父節點中,HK、NQ分開為兩個節點


    4.jpg
  5. 如步驟四的圖中可以繼續插入F,W,L,T


    5.jpg
  6. 在步驟五之后,插入Z就得進行分裂,T上移到父節點


    6.jpg
  7. 如步驟六的圖中插入D,進行分裂,D上移到父節點中,然后插入后續的P,R,X,Y節點沒有分裂


    7.jpg
  8. 插入最后一個S,含有N,P,Q,R的節點需要分裂,Q上移,導致父節點D,G,M,T也滿了,也需要進行分裂,繼續將中間元素M上移,產生新的節點,樹高度再加一層。


    8.jpg

B樹的刪除
首先查找B樹中要刪除的元素,若元素存在,則進行刪除。刪除該元素后,需要判斷該元素是否有左右孩子節點

  • 如果有,則上移孩子節點中的相近元素(左孩子中最右邊的節點或者右孩子中最左邊的節點)到父節點中去,移動之后的情況。
  • 如果沒有,直接刪除,移動之后的情況。

刪除元素,然后進行元素移動之后,如果節點關鍵字數目不滿足條件(小于ceil(m/2)-1),則需要看其相鄰的兄弟節點是否豐滿(關鍵字個數大于ceil(m/2)-1)

  • 如果豐滿,則向父節點借一個元素來滿足
  • 如果其相鄰兄弟都剛脫貧,即借了之后其結點數目小于ceil(m/2)-1,則該結點與其相鄰的某一兄弟結點進行“合并”成一個結點,以此來滿足條件。

對剛剛插入的樹進行刪除操作,依次刪除H,T,R,E

  1. 刪除H,在葉子節點H,K,L中,刪除后還剩兩個關鍵字,能夠滿足不小于ceil(m/2)-1=2,進行簡單的刪除元素后面的元素向前移動即可。


    d1.jpg
  2. 刪除T,QT節點不滿足關鍵字要求,需要上移孩子節點中相近元素W


    d2.jpg
  3. 刪除R,刪除后RS節點只剩一個關鍵字,根據上面的分析,兄弟節點豐滿,就向父節點借一個W,同時X需要上移到父節點中去。


    d3.jpg
  4. 刪除E,刪除后EF節點只剩一個關鍵字,根據上面分析,兄弟節點剛脫貧,則需要跟相鄰兄弟節點合并,D在兩個需要合并的節點之間,所以需要下移到之前的AC節點中,將僅剩的F進行合并,形成ACDF節點


    d4.jpg

    但是我們發現中間有一個節點只包含一個關鍵字,并且該節點非根節點,這個就需要進行修改。接下來進行分析:如果相鄰兄弟節點豐滿,可以從父節點中進行借一個元素,但是我們右邊的QX節點并不豐滿,所以只能下移M節點,減少樹的高度。最終圖如下:


    d5.jpg

B+樹
B樹的一種變形樹,m階的B+樹和m階的B樹區別:

  1. 所有葉子節點包含全部關鍵字信息,及指向含有這些關鍵字記錄的指針,且葉子節點中關鍵字進行有序鏈接
  2. 非葉子結點相當于是葉子結點的索引(稀疏索引),葉子結點相當于是存儲(關鍵字)數據的數據層;
B+樹.jpg

B+樹比B樹更適合操作系統的文件索引和數據庫索引的原因:

  • B+樹的磁盤讀寫代價更低,B+樹的內部節點沒有指向關鍵字具體信息的指針,因此內部節點相對B樹更小。如果把所有同一內部節點的關鍵字放在同一塊磁盤中,盤塊所能容納的關鍵字數量也就越多,一次性讀入內存中的需要查找的關鍵字也就越多,相對IO讀寫次數降低

舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤塊。而B+
樹內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B 樹就比B+ 樹多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)。

  • B+樹的查詢效率更加穩定
    由于非終結點并不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。

總而言之,B樹在提高了磁盤IO性能的同時并沒有解決元素遍歷的效率低下的問題。正是為了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷,支持基于范圍的查詢,而B樹不支持range-query這樣的操作(或者說效率太低)。

B*
B*樹是B+樹的變體,在B+樹的非根和非葉子結點再增加指向兄弟的指針;

B*樹.jpg

B+樹的分裂:當一個結點滿時,分配一個新的結點,并將原結點中1/2的數據復制到新結點,最后在父結點中增加新結點的指針;B+樹的分裂只影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針。

B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那么將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最后修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字范圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,并各復制1/3的數據到新結點,最后在父結點增加新結點的指針。

總結

  • B-樹:多路搜索樹,每個結點存儲M/2到M個關鍵字,非葉子結點存儲指向關鍵字范圍的子結點;所有關鍵字在整顆樹中出現,且只出現一次,非葉子結點可以命中;

  • B+樹:在B-樹基礎上,為葉子結點增加鏈表指針,所有關鍵字都在葉子結點 中出現,非葉子結點作為葉子結點的索引;B+樹總是到葉子結點才命中;

  • B*樹:在B+樹基礎上,為非葉子結點也增加鏈表指針,將結點的最低利用率從1/2提高到2/3;

借鑒于July大神的分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容