一文搞懂B樹、B-樹、B+樹


前言

B樹和B-樹是同一種數據結構,如果不清楚的話,會被面試官忽悠,所以本文介紹兩種數據結構,B樹和B+樹,廢話不多數咱們開干。

B樹

介紹

在計算機科學中,B樹是一種自平衡的樹,能夠保持數據有序。這種數據結構能夠讓查找數據、順序訪問、插入數據及刪除的動作,都在對數量級的時間復雜度內完成。B樹,其實是一顆特殊的二叉查找樹(binary search tree),可以擁有多於2個子節點。與自平衡二叉查找樹不同,B樹為系統大塊數據的讀寫操作做了優化。B樹減少定位記錄時所經歷的中間過程,從而加快存取速度,其實B樹主要解決的就是數據IO的問題。B樹這種數據結構可以用來描述外部存儲。這種數據結構常被應用在數據庫和文件系統的實現上。

特性

一個m階的B樹特點如下:

  1. 所有葉子節點都在同一層級;
  2. 除了根節點以外的其他節點包含的key值數量在[m/2]-1到m-1的數據范圍;
  3. 除了根節點和葉子節點外,所有中間節點至少有m/2個孩子節點;
  4. 根節點如果不是葉子節點的話,它必須包含至少2個孩子節點;
  5. 擁有n-1個key值非葉子節點必須有n個孩子節點;
  6. 一個節點的所有key值必須是升序排序的;
    以上六點就是B樹的全部特性

檢索

在B樹種,檢索操作類類似於二叉查找樹。在二叉查找樹中,檢索開始於樹的根節點,因為是二叉樹所以每次有兩種選擇。在B樹種檢索中,也是開始於根節點,但每次需要比較n次(n是當前節點的所有子節點的數量)。在B樹中,檢索操作執行的時間復雜度是O(log n),檢索操作執行如下:

  1. 從輸入獲取讀取檢索元素;
  2. 和根節點的第一個元素進行比較;
  3. 如果匹配上,返回元素找到並且終止函數;
  4. 如果未匹配上,檢查元數據與key值的大小;
  5. 如果小於待查元素,繼續檢索B樹的左子樹;
  6. 如果大於的話,比較相同節點中下一個key值並且重復3、4、5、6步,直到找到指定元素或者在樹的葉子節點的最后一個結束;

插入

在B樹種的插入操作,新元素一定是新添加在葉子節點的,具體操作流程如下:

  1. 檢查樹是否為空;
  2. 如果樹為空,用新的插入元素創建一個新的節點並插入到樹中並作為樹的根節點;
  3. 如果樹不為空,使用二叉查找樹的邏輯為新元素找到一個葉子節點;
  4. 如果葉子節點中key有空位置,直接按照葉子節點內key值升序的原則將節點插入;
  5. 如果葉子節點中key已經滿了,通過發送中間值到父節點,然后分裂葉子節點;重復這個操作,直到發送的中間值存儲在一個節點中;
  6. 如果分裂發生在根節點,中間值將會成為樹的新的根節點,樹的高度將會加 1;

具體操作的演示示例如下所示:

刪除

  1. 如果當前需要刪除的key位於非葉子節點上,則用后繼key(后繼key,即右邊葉子節點最左側即為后繼節點)覆蓋要刪除的key,然后在后繼key所在的子支中刪除該后繼key。此時后繼key一定位於葉子節點上,這個過程和二叉搜索樹刪除節點的方式類似。刪除這個記錄后執行第2步;
  2. 該節點key個數大於等於Math.ceil(m/2)-1(向下取整),結束刪除操作,否則執行第3步;
  3. 如果兄弟節點key個數大於Math.ceil(m/2)-1,則父節點中的key下移到該節點,兄弟節點中的一個key上移,刪除操作結束,即從兄弟節點中借一個元素過來;否則,將父節點中的key下移與當前節點及它的兄弟節點中的key合並,形成一個新的節點。原父節點中的key的兩個孩子指針就變成了一個孩子指針,指向這個新節點。然后當前節點的指針指向父節點,重復上第2步。

有些節點它可能即有左兄弟,又有右兄弟,那么我們任意選擇一個兄弟節點進行操作即可。
下面以5階B樹為例,介紹B樹的刪除操作,5階B樹中,節點最多有4個key,最少有2個key

a)原始狀態

b)在上面的B樹中刪除21,刪除后節點中的關鍵字個數仍然大於等2,所以刪除結束。

c)在上述情況下接着刪除27。從上圖可知27位於非葉子節點中,所以用27的后繼替換它。從圖中可以看出,27的后繼為28,我們用28替換27,然后在28(原27)的右孩子節點中刪除28。刪除后的結果如下圖所示。

刪除后發現,當前葉子節點的記錄的個數小於2,而它的兄弟節點中有3個記錄(當前節點還有一個右兄弟,選擇右兄弟就會出現合並節點的情況,不論選哪一個都行,只是最后B樹的形態會不一樣而已),我們可以從兄弟節點中借取一個key。所以父節點中的28下移,兄弟節點中的26上移,刪除結束。結果如下圖所示。

d)在上述情況下接着32,結果如下圖。

當刪除后,當前節點中只key,而兄弟節點中也僅有2個key。所以只能讓父節點中的30下移和這個兩個孩子節點中的key合並,成為一個新的節點,當前節點的指針指向父節點。結果如下圖所示。

當前節點key的個數滿足條件,故刪除結束。

e)上述情況下,我們接着刪除key為40的記錄,刪除后結果如下圖所示。

同理,當前節點的記錄數小於2,兄弟節點中沒有多余key,所以父節點中的key下移,和兄弟(這里我們選擇左兄弟,選擇右兄弟也可以)節點合並,合並后的指向當前節點的指針就指向了父節點。

同理,對於當前節點而言只能繼續合並了,最后結果如下所示。

合並后節點當前節點滿足條件,刪除結束。

小結

以上就是B樹的特征以及操作過程,應該是說明白了,在生產環境中直接應用B樹的場景比較少,但是B樹是B+樹的基礎,所有很有必要把B樹搞明白。

B+樹

B+樹是B樹的變種,但不同資料中B+樹的定義各有不同,其差異在於節點中關鍵字個數和孩子節點個數。一種是節點中關鍵字個數和孩子個數相同,另一種是關鍵字個數比孩子節點個數小1,這種方式是和B樹基本相同。

特性

  1. B+樹包含2種類型的節點:內部節點(也稱索引節點)和葉子節點。根節點本身即可以是內部節點,也可以是葉子節點。根節點的關鍵字key個數最少可以只有1個;
  2. B+樹與B樹最大的不同是內部節點不保存數據,只用於索引,所有數據(或者說記錄)都保存在葉子節點中;
  3. m階B+樹表示了內部節點最多有m-1個關鍵字(或者說內部節點最多有m個子樹,和B樹相同),階數m同時限制了葉子節點最多存儲m-1個記錄;
  4. 內部節點中的key都按照從小到大的順序排列,對於內部節點中的一個key,左樹中的所有key都小於它,右子樹中的key都大於等於它。葉子節點中的記錄也按照key的大小排列;
  5. 每個葉子節點都存有相鄰葉子節點的指針,葉子節點本身依關鍵字的大小自小而大順序鏈接;

插入

  1. 若為空樹,創建一個葉子節點,然后將記錄插入其中,此時這個葉子節點也是根節點,插入操作結束。
  2. 針對葉子類型節點:根據key值找到葉子節點,向這個葉子節點插入記錄。插入后,若當前節點key的個數小於等於m-1,則插入結束。否則將這個葉子節點分裂成左右兩個葉子節點,左葉子節點包含前m/2個記錄,右節點包含剩下的記錄,將第m/2+1個記錄的key進位到父節點中(父節點一定是索引類型節點),進位到父節點的key左孩子指針向左節點, 右孩子指針向右節點。將當前節點的指針指向父節點,然后執行第3步。
  3. 針對索引類型節點:若當前節點key的個數小於等於m-1,則插入結束。否則,將這個索引類型節點分裂成兩個索引節點,左索引節點包含前(m-1)/2個key,右節點包含m-(m-1)/2個key,將第m/2個key進位到父節點中,進位到父節點的key左孩子指向左節點, 進位到父節點的key右孩子指向右節點。將當前節點的指針指向父節點,然后重復第3步,直到插入結束。

插入過程相對簡單,這里不再舉例。

刪除

B+樹的刪除算法和B樹的刪除算法基本類似。葉子節點中如果沒有相應的key值,刪除失敗。否則執行下面的步驟:

  1. 刪除葉子節點中對應的key。刪除后若節點的key的個數大於等於Math.ceil(m-1)/2 – 1,刪除操作結束,否則執行第2步。
  2. 若兄弟節點key有富余(大於Math.ceil(m-1)/2 – 1),向兄弟節點借一個記錄,同時用借到的key替換父結(指當前節點和兄弟節點共同的父節點)點中的key,刪除結束。否則執行第3步。
  3. 若兄弟節點中沒有富余的key,則當前節點和兄弟節點合並成一個新的葉子節點,並刪除父節點中的key(父節點中的這個key兩邊的孩子指針就變成了一個指針,正好指向這個新的葉子節點),將當前節點指向父節點(必為索引節點),執行第4步(第4步以后的操作和B樹就完全一樣了,主要是為了更新索引節點)。
  4. 若索引節點的key的個數大於等於Math.ceil(m-1)/2 – 1,則刪除操作結束。否則執行第5步
  5. 若兄弟節點有富余,父節點key下移,兄弟節點key上移,刪除結束。否則執行第6步
  6. 當前節點和兄弟節點及父節點下移key合並成一個新的節點。將當前節點指向父節點,重復第4步。

注意,通過B+樹的刪除操作后,索引節點中存在的key,不一定在葉子節點中存在對應的記錄。

下面通過一棵5階B+樹來說明上述算法的刪除過程,5階B+數的節點最少2個key,最多4個key。
a)初始狀態

b)刪除22,刪除后結果如下圖

刪除后葉子結點中key的個數大於等於2,刪除結束

c)刪除15,刪除后的結果如下圖所示

刪除后當前結點只有一個key,不滿足條件,而兄弟結點有三個key,可以從兄弟結點借一個關鍵字為9的記錄,同時更新將父結點中的關鍵字由10也變為9,刪除結束。

d)刪除7,刪除后的結果如下圖所示

當前結點關鍵字個數小於2,(左)兄弟結點中的也沒有富余的關鍵字(當前結點還有個右兄弟,不過選擇任意一個進行分析就可以了,這里我們選擇了左邊的),所以當前結點和兄弟結點合並,並刪除父結點中的key,當前結點指向父結點。

此時當前結點的關鍵字個數小於2,兄弟結點的關鍵字也沒有富余,所以父結點中的關鍵字下移,和兩個孩子結點合並,結果如下圖所示。

小結

以上是B+樹的增刪操作以及特性,在生產環境中常用做創建索引,與磁盤進行io。

對比

相比於二叉排序樹,B樹為系統最優化大塊數據的讀和寫操作。B-tree算法減少定位記錄時所經歷的中間過程,從而加快存取速度,普遍運用在數據庫和文件系統。

B和B+樹的區別在於,B+樹的非葉子結點只包含導航信息,不包含實際的值,所有的葉子結點和相連的節點使用鏈表相連,便於區間查找和遍歷。B+ 樹的優點在於:
由於B+樹在內部節點上不包含數據信息,因此在內存頁中能夠存放更多的key。 數據存放的更加緊密,具有更好的空間局部性。因此訪問葉子幾點上關聯的數據也具有更好的緩存命中率。
B+樹的葉子結點都是相鏈的,因此對整棵樹的便利只需要一次線性遍歷葉子結點即可。而且由於數據順序排列並且相連,所以便於區間查找和搜索。而B樹則需要進行每一層的遞歸遍歷。相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有B+樹好。

但是B樹也有優點,其優點在於,由於B樹的每一個節點都包含key和value,因此經常訪問的元素可能離根節點更近,因此訪問也更迅速。

對於一顆節點為N度為M的子樹,查找和插入需要log(M-1) N ~ log(M/2) N次比較。這個很好證明,對於度為M的B樹,每一個節點的子節點個數為M/2 到 M-1之間,所以樹的高度在log(M-1) N至log(M/2) N之間。

這種效率是很高的,對於N=62*1000000000個節點,如果度為1024,則logM/2N <=4,即在620億個元素中,如果這棵樹的度為1024,則只需要小於4次即可定位到該節點,然后再采用二分查找即可找到要找的值。

應用場景

B樹和B+廣泛應用於文件存儲系統以及數據庫系統中,在講解應用之前,我們看一下常見的存儲結構:

我們計算機的主存基本都是隨機訪問存儲器(Random-Access Memory,RAM),他分為兩類:靜態隨機訪問存儲器(SRAM)和動態隨機訪問存儲器(DRAM)。SRAM比DRAM快,但是也貴的多,一般作為CPU的高速緩存,DRAM通常作為內存。這類存儲器他們的結構和存儲原理比較復雜,基本是使用電信號來保存信息的,不存在機器操作,所以訪問速度非常快,具體的訪問原理可以查看CSAPP,另外,他們是易失的,即如果斷電,保存DRAM和SRAM保存的信息就會丟失。

我們使用的更多的是使用磁盤,磁盤能夠保存大量的數據,從GB一直到TB級,但是他的讀取速度比較慢,因為涉及到機器操作,讀取速度為毫秒級,從DRAM讀速度比從磁盤度快10萬倍,從SRAM讀速度比從磁盤讀快100萬倍。下面來看下磁盤的結構:

如上圖,磁盤由盤片構成,每個盤片有兩面,又稱為盤面(Surface),這些盤面覆蓋有磁性材料。盤片中央有一個可以旋轉的主軸(spindle),他使得盤片以固定的旋轉速率旋轉,通常是5400轉每分鍾(Revolution Per Minute,RPM)或者是7200RPM。磁盤包含一個多多個這樣的盤片並封裝在一個密封的容器內。上圖左,展示了一個典型的磁盤表面結構。每個表面是由一組成為磁道(track)的同心圓組成的,每個磁道被划分為了一組扇區(sector).每個扇區包含相等數量的數據位,通常是(512)子節。扇區之間由一些間隔(gap)隔開,不存儲數據。

以上是磁盤的物理結構,現在來看下磁盤的讀寫操作:

如上圖,磁盤用讀/寫頭來讀寫存儲在磁性表面的位,而讀寫頭連接到一個傳動臂的一端。通過沿着半徑軸前后移動傳動臂,驅動器可以將讀寫頭定位到任何磁道上,這稱之為尋道操作。一旦定位到磁道后,盤片轉動,磁道上的每個位經過磁頭時,讀寫磁頭就可以感知到位的值,也可以修改值。對磁盤的訪問時間分為 尋道時間,旋轉時間,以及傳送時間。

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,因此為了提高效率,要盡量減少磁盤I/O,減少讀寫操作。為了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向后讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理:

當一個數據被用到時,其附近的數據也通常會馬上被使用。

程序運行期間所需要的數據通常比較集中。

由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

預讀的長度一般為頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割為連續的大小相等的塊,每個存儲塊稱為一頁(在許多操作系統中,頁得大小通常為4k),主存和磁盤以頁為單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向后連續讀取一頁或幾頁載入內存中,然后異常返回,程序繼續運行。

文件系統及數據庫系統的設計者利用了磁盤預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

每次新建一個節點的同時,直接申請一個頁的空間( 512或者1024),這樣就保證一個節點物理上也存儲在一個頁里,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。如,將B樹的度M設置為1024,這樣在前面的例子中,600億個元素中只需要小於4次查找即可定位到某一存儲位置。

同時在B+樹中,內節點只存儲導航用到的key,並不存儲具體值,這樣內節點個數較少,能夠全部讀取到主存中,外接點存儲key及值,並且順序排列,具有良好的空間局部性。所以B及B+樹比較適合與文件系統的數據結構。下面是一顆B樹,用來進行內容存儲。

另外B/B+樹也經常用做數據庫的索引,在mysql中的B+樹索引構建就采用的這種數據結構,會在后續文章中進行詳細介紹.

總結

本文詳細介紹了B樹、B+樹的特征、操作、以及使用場景,參考了很多前人寫的博客,希望對大家能有所幫助。
參考博客如下:
B+樹詳解
B樹詳解
B樹介紹


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM