1. 程式人生 > >重溫資料結構:理解 B 樹、B+ 樹特點及使用場景

重溫資料結構:理解 B 樹、B+ 樹特點及使用場景

讀完本文你將瞭解:

大家好,前面那篇文章《3 分鐘理解完全二叉樹、平衡二叉樹、二叉查詢樹》中我們瞭解了幾種特殊的二叉樹的功能及特點,知道了它們在進行查詢資料時可以提高效率,但需要注意的是,這是指在記憶體中進行查詢。如果有海量的資料,不可能一次性讀取到記憶體中,這時候就要考慮的是,如何在磁碟中快速找到需要的資料。

今天這篇文章中要介紹的“B 樹、B+ 樹”,他們的使用場景是:查詢磁碟中的大量資料。

B 樹

B 樹就是常說的“B 減樹(B- 樹)”,又名平衡多路(即不止兩個子樹)查詢樹,它和平衡二叉樹的不同有這麼幾點:

  1. 平衡二叉樹節點最多有兩個子樹,而 B 樹每個節點可以有多個子樹,M 階 B 樹表示該樹每個節點最多有 M 個子樹
  2. 平衡二叉樹每個節點只有一個數據和兩個指向孩子的指標,而 B 樹每個中間節點有 k-1 個關鍵字(可以理解為資料)和 k 個子樹( **k 介於階數 M 和 M/2 之間,M/2 向上取整)
  3. B 樹的所有葉子節點都在同一層,並且葉子節點只有關鍵字,指向孩子的指標為 null

和平衡二叉樹相同的點在於:B 樹的節點資料大小也是按照左小右大,子樹與節點的大小比較決定了子樹指標所處位置。

看著概念可能有點難理解,來看看圖對比下平衡二叉樹和 B 樹。

對比平衡二叉樹和 B 樹

首先是節點, 平衡二叉樹的節點如下圖所示,每個節點有一個數據和最多兩個子樹:

這裡寫圖片描述

B 樹中的每個節點由兩部分組成:

  1. 關鍵字(可以理解為資料)
  2. 指向孩子節點的指標

B 樹的節點如下圖所示,每個節點可以有不只一個數據,同時擁有資料數加一個子樹,同時每個節點左子樹的資料比當前節點都小、右子樹的資料都比當前節點的資料大:

這裡寫圖片描述

上圖是為了方便讀者理解 B 樹每個節點的內容,實際繪製圖形還是以圓表示每個節點。

瞭解了節點的差異後,來看看 B 樹的定義,一棵 B 樹必須滿足以下條件

  1. 若根結點不是終端結點,則至少有2棵子樹
  2. 除根節點以外的所有非葉結點至少有 M/2 棵子樹,至多有 M 個子樹(關鍵字數為子樹減一)
  3. 所有的葉子結點都位於同一層

用一張圖對比平衡二叉樹和 B 樹:

這裡寫圖片描述

可以看到,B 樹的每個節點可以表示的資訊更多,因此整個樹更加“矮胖”,這在從磁碟中查詢資料(先讀取到記憶體、後查詢)的過程中,可以減少磁碟 IO 的次數,從而提升查詢速度。

B 樹中如何查詢資料

因為 B 樹的子樹大小排序規則,因此在 B 樹中查詢資料時,一般需要這樣:

  1. 從根節點開始,如果查詢的資料比根節點小,就去左子樹找,否則去右子樹
  2. 和子樹的多個關鍵字進行比較,找到它所處的範圍,然後去範圍對應的子樹中繼續查詢
  3. 以此迴圈,直到找到或者到葉子節點還沒找到為止

B 樹如何保證平衡

我們知道,平衡的樹之所以能夠加快查詢速度,是因為在新增、刪除的時候做了某些操作以保證平衡。

平衡二叉樹的平衡條件是:左右子樹的高度差不大於 1;而 B 樹的平衡條件則有三點:

  1. 葉子節點都在同一層
  2. 每個節點的關鍵字數為子樹個數減一(子樹個數 k 介於樹的階 M 和它的二分之一
  3. 子樹的關鍵字保證左小右大的順序

也就是說,一棵 3 階的 B 樹(即節點最多有三個子樹),每個節點的關鍵字數最少為 1,最多為 2,如果要新增資料的子樹的關鍵字數已經是最多,就需要拆分節點,調整樹的結構。

網上找到一張很不錯的動圖,我們來根據它分析下 B 樹新增元素時如何保證平衡。

這個圖用以表示往 4 階 B 樹中依次插入下面這組資料的過程:

6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4

這裡寫圖片描述

建議放大圖檢視

由於我比較懶,我們來根據前幾步分析下 B 樹的新增流程:

  1. 首先明確:4 階 B 樹表示每個節點最多有 4 個子樹、3 個關鍵字,最少有 2 個子樹、一個關鍵字
  2. 新增 6,第一個節點,沒什麼好說的
  3. 新增 10,根節點最多能放三個關鍵字,按順序添到根節點中
  4. 新增 4,還能放到根節點中
  5. 新增 14,這時超出了關鍵字最大限制,需要把 14 新增為子樹,同時為了保證“所有葉子節點在同一層”,就需要拆幾個關鍵字作為子樹:

這裡寫圖片描述 拆為:這裡寫圖片描述

這個拆的過程比較複雜,首先要確定根節點保留幾個關鍵字,由於“非葉子節點的根節點至少有 2 棵子樹”的限制,那就至少需要兩個關鍵字分出去,又因為“子樹數是關鍵字數+1”,如果根節點有兩個關鍵字,就得有三個子樹,無法滿足,所以只好把除 6 以外的三個關鍵字都拆為子樹。

誰和誰在一個子樹上呢,根據“左子樹比關鍵字小、右子樹比關鍵字大”的規律,4 在左子樹,10 和 14 在右子樹。

繼續新增 :

  1. 新增 5,放到 4 所在的子樹上
  2. 新增 11,放在 10 和 14 所在的右子樹上
  3. 新增 15,按大小應該放到 10、11 和 14 所在的子樹上,但因為超過了關鍵字數限制,又得拆分

這裡寫圖片描述

因為“根節點必須都在同一層”,因此我們不能給現有的左右子樹新增子樹,只能新增給 6 了;但是如果 6 有三個子樹,就必須得有 2 個關鍵字,提升誰做關鍵字好呢,這得看誰做 6 中間的子樹,因為右子樹的所有關鍵字都得比父節點的關鍵字大,所以這個提升的關鍵字只能比未來右子樹中的關鍵字都小,那就只有 10 和 11 可以考慮了。

提升 10 吧,沒有比它小的做子樹,那就只能提升 11 了:

這裡寫圖片描述

再新增元素也是類似的邏輯:

  1. 首先考慮要插入的子樹是否已經超出了關鍵字數的限制
  2. 超出的話,如果要插入的位置是葉子節點,就只能拆一個關鍵字新增到要插入位置的父節點
  3. 如果非葉子節點,就得從其他子樹拆子樹給新插入的元素做孩子

刪除也是一樣的,要考慮刪除孩子後,父節點是否還滿足子樹 k 介於 M/2 和 M 的條件,不滿足就得從別的節點拆子樹甚至修改相關子樹結構來保持平衡。

總之新增、刪除的過程很複雜,要考慮的條件很多,具體實現就不細追究了,這裡我們有個基本認識即可。

正是這個複雜的保持平衡操作,使得平衡後的 B 樹能夠發揮出磁碟中快速查詢的作用。

使用場景

檔案系統和資料庫系統中常用的B/B+ 樹,他通過對每個節點儲存個數的擴充套件,使得對連續的資料能夠進行較快的定位和訪問,能夠有效減少查詢時間,提高儲存的空間區域性性從而減少IO操作。他廣泛用於檔案系統及資料庫中,如:

  • Windows:HPFS 檔案系統

  • Mac:HFS,HFS+ 檔案系統

  • Linux:ResiserFS,XFS,Ext3FS,JFS 檔案系統

  • 資料庫:ORACLE,MYSQL,SQLSERVER 等中

  • 資料庫:ORACLE,MYSQL,SQLSERVER 等中

B+ 樹

這部分主要學習自“程式設計師小灰”的 漫畫:什麼是B+樹?

瞭解了 B 樹後再來了解下它的變形版:B+ 樹,它比 B 樹的查詢效能更高。

一棵 B+ 樹需要滿足以下條件:

  1. 節點的子樹數和關鍵字數相同(B 樹是關鍵字數比子樹數少一)
  2. 節點的關鍵字表示的是子樹中的最大數,在子樹中同樣含有這個資料
  3. 葉子節點包含了全部資料,同時符合左小右大的順序

這裡寫圖片描述

簡單概括下 B+ 樹的三個特點:

  1. 關鍵字數和子樹相同
  2. 非葉子節點僅用作索引,它的關鍵字和子節點有重複元素
  3. 葉子節點用指標連在一起

首先第一點不用特別介紹了,在 B 樹中,節點的關鍵字用於在查詢時確定查詢區間,因此關鍵字數比子樹數少一;而在 B+ 樹中,節點的關鍵字代表子樹的最大值,因此關鍵字數等於子樹數。

第二點,除葉子節點外的所有節點的關鍵字,都在它的下一級子樹中同樣存在,最後所有資料都儲存在葉子節點中。

根節點的最大關鍵字其實就表示整個 B+ 樹的最大元素。

第三點,葉子節點包含了全部的資料,並且按順序排列,B+ 樹使用一個連結串列將它們排列起來,這樣在查詢時效率更快。

由於 B+ 樹的中間節點不含有實際資料,只有子樹的最大資料和子樹指標,因此磁碟頁中可以容納更多節點元素,也就是說同樣資料情況下,B+ 樹會 B 樹更加“矮胖”,因此查詢效率更快。

B+ 樹的查詢必會查到葉子節點,更加穩定。

有時候需要查詢某個範圍內的資料,由於 B+ 樹的葉子節點是一個有序連結串列,只需在葉子節點上遍歷即可,不用像 B 樹那樣挨個中序遍歷比較大小。

B+ 樹的三個優點:

  1. 層級更低,IO 次數更少
  2. 每次都需要查詢到葉子節點,查詢效能穩定
  3. 葉子節點形成有序連結串列,範圍查詢方便

新增過程就不深入研究了,後面用到再看吧,這裡先貼一個 B+ 樹動態新增元素圖:

這裡寫圖片描述

Thanks