多路平衡查找樹(下面簡稱B-Tree)是一棵自平衡樹,可以認爲是平衡二叉樹的泛化版。一棵m階B樹(balanced tree of order m)是一棵平衡的m路搜索樹。它或者是空樹,或者是滿足下列性質的樹:
- 根結點至少有兩個子女;
- 每個非根節點所包含的關鍵字個數 j 滿足:┌m/2┐ - 1 <= j <= m - 1;
- 除根結點以外的所有結點(不包括葉子結點)的度數正好是關鍵字總數加1,故內部子樹個數 k 滿足:┌m/2┐ <= k <= m ;
- 所有的葉子結點都位於同一層。
假設有一棵平衡的3路搜索樹,他的結構如圖
上圖是一個簡單的B-Tree,我們通過圖對B-Tree的定義對號入座,可以看到滿足如下:
- 根結點有兩個子女。
- 所有非根節點,關鍵字個數滿足┌3/2┐ - 1 <= 2 <= 3 - 1。
- 除根結點以外的所有結點(不包括葉子結點)的子節點數正好是關鍵字總數加1。
- 所有的葉子結點都位於同一層。
- 對於N個關鍵字的B-Tree,它的葉子節點數是N+1。17個關鍵字 + 1 = 18個葉子節點。
- B-Tree中,所有關鍵字以從小到大的順序排列。
- 對於一棵B-Tree,它的查找效率是log┌M/2┐((N+1)/2 )+1。M是路數, N是關鍵字個數。
B-Tree需要滿足定義和保持平衡,所以在進行插入或者刪除時,需要作出相應的調整使樹滿足B-Tree的條件並保持平衡。
插入關鍵字:
插入關鍵字主要分爲兩種情況,假設節點S當前的關鍵字個數爲J <= M-1, 要插入的關鍵字爲K:
- 若J < M-1, 則直接在對應位置插入K。
- 若J = M-1, 則節點會產生頁分裂:此時需要找出S節點原始關鍵字和K關鍵字的中位數關鍵字Kmid,Kmid左右兩邊分裂成兩個節點,並將Kmid節點插入到S節點的父節點對應的位置。父節點遞歸上述過程。
刪除關鍵字:
刪除關鍵字也分爲兩種情況,假設當前節點爲S,要刪除的關鍵字爲K:
- 若S是葉子節點,則直接刪除,假如發生了下溢(不滿足┌m/2┐ - 1 <= j <= m - 1),則需要調整再平衡。
- 若S是內部節點,K是其左右兩個子節點的分隔符,因此需要選取左節點的最大值或者右節點的最小值替換K節點作爲分隔符,若子節點發生了下溢,則需要調整再平衡。
刪除調整再平衡:
- 如果缺少元素節點的右兄弟存在且擁有多餘的元素,那麼向左旋轉
- 將父節點的分隔值複製到缺少元素節點的最後(分隔值被移下來;缺少元素的節點現在有最小數量的元素)
- 將父節點的分隔值替換爲右兄弟的第一個元素(右兄弟失去了一個節點但仍然擁有最小數量的元素)
- 樹又重新平衡
- 否則,如果缺少元素節點的左兄弟存在且擁有多餘的元素,那麼向右旋轉
- 將父節點的分隔值複製到缺少元素節點的第一個節點(分隔值被移下來;缺少元素的節點現在有最小數量的元素)
- 將父節點的分隔值替換爲左兄弟的最後一個元素(左兄弟失去了一個節點但仍然擁有最小數量的元素)
- 樹又重新平衡
- 否則,如果它的兩個直接兄弟節點都只有最小數量的元素,那麼將它與一個直接兄弟節點以及父節點中它們的分隔值合併
- 將分隔值複製到左邊的節點(左邊的節點可以是缺少元素的節點或者擁有最小數量元素的兄弟節點)
- 將右邊節點中所有的元素移動到左邊節點(左邊節點現在擁有最大數量的元素,右邊節點爲空)
- 將父節點中的分隔值和空的右子樹移除(父節點失去了一個元素)
- 如果父節點是根節點並且沒有元素了,那麼釋放它並且讓合併之後的節點成爲新的根節點(樹的深度減小)
- 否則,如果父節點的元素數量小於最小值,重新平衡父節點
對於B-Tree的思考:
B-Tree是平衡二叉樹的泛化變體,從查找效率上來說,平衡二叉樹是優於B-Tree,但是在一些條件下,B-Tree的執行表現會好很多,例如,目前大多數數據庫存儲引擎的實現都採用B-Tree或者B+-Tree(B-Tree的變形),而不是直接採用平衡二叉樹。在IO密集型的系統中,限制系統運行效率的關鍵往往是IO的速度,一次IO花費的時間往往是CPU時間的幾十上百萬倍,因此,在進行數據的讀取時,會預讀跟數據相鄰的數據,通過局部性原理,提高磁盤訪問的效率(查詢次數/磁盤讀取次數)。B-Tree的結構滿足了這種場景的需要,雖然查找次數提高了一些,但相比昂貴的磁盤IO,是非常值得的。