B树、B+树其实很简单,看不懂你找我
一. B樹的定義
1.1. B樹概念與使用場景
B樹(B-tree,所以很多人又稱為B-樹)
是一種自平衡的樹,一個節點可以擁有2個以上的子節點,能夠保持數據有序。這種數據結構能夠讓查找數據、順序訪問、插入數據及刪除的動作,都在對數時間內完成。
與自平衡二叉查找樹不同,B樹的每個節點可以包含大量的關鍵字信息和分支,便于降低自己的高度,讓自己更胖更矮,更加適用于讀寫相對大的數據塊的存儲系統,例如磁盤,可以減少定位記錄時所經歷的中間過程,從而加快存取速度。B樹這種數據結構可以用來描述外部存儲。這種數據結構常被應用在數據庫和文件系統的實現上。
在B樹中,內部(非葉子)節點可以擁有可變數量的子節點(數量范圍預先定義好)。當數據被插入或從一個節點中移除,它的子節點數量發生變化。為了維持在預先設定的數量范圍內,內部節點可能會被合并或者分離。因為子節點數量有一定的允許范圍,所以B樹不需要像其他自平衡查找樹那樣頻繁地重新保持平衡,在這點名批評紅黑樹,但是由于節點沒有被完全填充,可能浪費了一些空間。
B樹的優勢如下:
- 使關鍵字保持排序順序以進行順序遍歷
- 使用分層索引來最大程度地減少磁盤讀取次數
- 使用部分完整的塊來加快插入和刪除
- 使用遞歸算法使索引保持平衡
總之,B樹操作不是很復雜,用途很廣泛,正道的光,照在了數據結構的路上
1.2. B樹定義
根據 Knuth 的定義,一個 m 階的B樹是一個有以下屬性的樹:
- 每一個節點最多有 m 個子節點
- 每一個非葉子節點(除根節點)最少有 ?m/2? (向上取整)個子節點
- 如果根節點不是葉子節點,那么它至少有兩個子節點
- 有 k 個子節點的非葉子節點擁有 k ? 1 個鍵
- 所有的葉子節點都在同一層,葉子節點不包含任何關鍵字信息
但是其實文獻中B樹的術語并不統一
歐美在術語統一這方面一直都可以的 : )
術語階的定義不一致。
- Bayer & McCreightComer等人將B樹的階定義為非根節點擁有鍵的最小數量。Folk & Zoellick指出這一術語是模糊不清的。一個 3 階B樹鍵的最大數量可能為 6 或 7。 Knuth 通過將階定義為最大數量的子節點(鍵值數的最大值+1) 來避免了這一問題。
術語葉子的定義也不一致。
- Bayer & McCreight認為葉子層是最下面一層的鍵,此時這些葉子節點只包含鍵值,子節點指針都指向不含任何信息的空的記錄。
- Knuth 認為葉子層是最下面一層鍵之下的一層。如同紅黑樹那般(紅黑樹將Null指針作為黑色葉子節點),將Null作為葉子節點,此時葉子節點不存儲鍵值等任何信息。
- 我們一般以按Knuth大佬的說法來,清華的嚴蔚敏老師的數據結構一書中也是這么定義的。
內部節點:
- 每個內部節點都至少含有 ?m/2? 個子節點,所以至少都是半滿的
- 當父節點還沒滿時,一個滿子節點可以被分為兩個合法子節點
- 每個內部節點(除葉子節點和根節點之外的所有節點)包含以下信息
- 指向父節點的指針Parent*
- 鍵值的個數KeyNum
- 鍵值Key
- KeyNum+1個指向子節點的指針Ptr*
根節點:
- 根節點的子節點數的最大值和內部節點一樣都是m
- 根節點的子節點數沒有限制,當根節點儲存的鍵值數小于?m/2?時,根節點可以沒有子節點,此時根節點為葉子節點。
二. B樹的操作
在B樹中,內部(非葉子)節點可以在某個預定義范圍內具有可變數量的子節點。 從節點插入或刪除數據時,其子節點數會更改。 為了維持預定范圍,可以將內部節點連接或拆分。
2.1. m階B樹的高度
對硬盤訪問的IO次數取決于B樹的高度,B樹的高度如何確定呢?
首先明確的是不帶任何信息的一層葉節點不算做B樹的高度,通常,設一個m階B樹非葉子節點內部的關鍵字范圍在d~2d范圍內,d = ?m/2?
設h為樹的高度(h≥0),空樹的高度表示為0n為樹中鍵值的總個數(n≥0),空樹的總鍵值數為0設h為樹的高度(h\ge0),空樹的高度表示為0\\n為樹中鍵值的總個數(n\ge0),空樹的總鍵值數為0設h為樹的高度(h≥0),空樹的高度表示為0n為樹中鍵值的總個數(n≥0),空樹的總鍵值數為0
當內部關鍵字為2d即m時,樹的高度有最小值當內部關鍵字為2d即m時,樹的高度有最小值當內部關鍵字為2d即m時,樹的高度有最小值
h=0時,有n0=0h=0時,有n_0=0h=0時,有n0?=0
h=1時,有n1=m?1h=1時,有n_1 = m-1h=1時,有n1?=m?1
h=2時,有n2=m(m?1)+n1=(m+1)(m?1)=m2?1h=2時,有n_2=m(m-1)+n_1=(m+1)(m-1)=m^2-1h=2時,有n2?=m(m?1)+n1?=(m+1)(m?1)=m2?1
h=3時,有n3=m2(m?1)+n2=(m2+m+1)(m?1)=m3?1h=3時,有n_3=m^2(m-1)+n_2=(m^2+m+1)(m-1)=m^3-1h=3時,有n3?=m2(m?1)+n2?=(m2+m+1)(m?1)=m3?1
…\dots…
h=h時,有n=mh?1h=h時,有n=m^{h}-1h=h時,有n=mh?1
所以hmin=logm(n+1)所以h_{min}=log_m(n+1)所以hmin?=logm?(n+1)
當內部關鍵字為d時即?m/2?時,樹的高度有最大值當內部關鍵字為d時即{\lceil m/2\rceil}時,樹的高度有最大值當內部關鍵字為d時即?m/2?時,樹的高度有最大值
h=0時,n0=0h=0時,n_0=0h=0時,n0?=0
h=1時,非葉子根節點最少兩個子節點和一個關鍵字,所以有n1=1h=1時,非葉子根節點最少兩個子節點和一個關鍵字,所以有n_1=1h=1時,非葉子根節點最少兩個子節點和一個關鍵字,所以有n1?=1
h=2時,有n2=2(?m/2??1)+1=2?m/2??1h=2時,有n_2=2({\lceil m/2\rceil}-1) + 1=2{\lceil m/2\rceil}-1h=2時,有n2?=2(?m/2??1)+1=2?m/2??1
h=3時,有n3=2?m/2?(?m/2??1)+n2=2?m/2?2?1h=3時,有n_3=2{\lceil m/2\rceil}({\lceil m/2\rceil}-1) + n_2=2{\lceil m/2\rceil}^2-1h=3時,有n3?=2?m/2?(?m/2??1)+n2?=2?m/2?2?1
…\dots…
n=2?m/2?h?1?1n=2{\lceil m/2\rceil}^{h-1}-1n=2?m/2?h?1?1
hmax=log?m/2?(n+12)+1h_{max}=log_{\lceil m/2\rceil}{(\frac{n+1}2)}+1hmax?=log?m/2??(2n+1?)+1
看上去很復雜,其實就是等差數列求和
2.2.插入
所有的插入都從根節點開始。要插入一個新的元素,首先搜索這棵樹找到新元素應該被添加到的對應節點。將新元素插入到這一節點中的步驟如下:
- 如果節點擁有的元素數量小于最大值,那么有空間容納新的元素。將新元素插入到這一節點,且保持節點中元素有序。
- 否則的話這一節點已經滿了,將它平均地分裂成兩個節點:
- 從該節點的原有元素和新的元素中選擇出中位數,小于這一中位數的元素放入左邊節點,大于這一中位數的元素放入右邊節點,中位數作為分隔值。
- 分隔值被插入到父節點中,這可能會造成父節點分裂,分裂父節點時可能又會使它的父節點分裂,以此類推。如果沒有父節點(這一節點是根節點),就創建一個新的根節點(增加了樹的高度)。如果分裂一直上升到根節點,那么一個新的根節點會被創建,它有一個分隔值和兩個子節點。這就是根節點并不像內部節點一樣有最少子節點數量限制的原因。
例子,已有4階B樹如下
插入4,根據分層查找關鍵字,直接插入:
插入17,根據分層查找關鍵字,直接插入:
插入20,子節點中關鍵字達到m-1,故無法繼續插入,子節點分割,16分至父節點:
插入21、22,直到分裂出新的根節點
2.3.刪除
刪除葉子節點中的元素
- 搜索要刪除的元素
- 如果它在葉子節點,將它從中刪除
- 如果發生了下溢出,按照后面 “刪除后重新平衡”部分的描述重新調整樹
刪除內部節點中的元素
內部節點中的每一個元素都作為分隔兩顆子樹的分隔值,因此我們需要重新劃分。值得注意的是左子樹中最大的元素仍然小于分隔值,右子樹中最小的元素仍然大于分隔值,這兩個元素都在葉子節點中,并且任何一個都可以作為兩顆子樹的新分隔值。算法的描述如下:
- 選擇一個新的分隔符(左子樹中最大的元素或右子樹中最小的元素),將它從子節點中移除,替換掉被刪除的元素作為新的分隔值。
- 前一步刪除了一個子節點中的元素。如果這個子節點擁有的元素數量小于最低要求,那么從這一子節點開始重新進行平衡。
刪除后的重新平衡
重新平衡從葉子節點開始向根節點進行,直到樹重新平衡。如果刪除節點中的一個元素使該節點的元素數量低于最小值,那么一些元素必須被重新分配。通常,移動一個元素數量大于最小值的兄弟節點中的元素。如果兄弟節點都沒有多余的元素,那么缺少元素的節點就必須要和他的兄弟節點合并。合并可能導致父節點失去了分隔值,所以父節點可能缺少元素并需要重新平衡。合并和重新平衡可能一直進行到根節點,根節點變成惟一缺少元素的節點。重新平衡樹的遞歸算法如下:
- 如果缺少元素節點的右兄弟存在且擁有多余的元素,那么向左旋轉,這里的左旋轉與AVL中的左旋轉類似。
- 將父節點的分隔值復制到缺少元素節點的最后(分隔值被移下來;缺少元素的節點現在有最小數量的元素)
- 將父節點的分隔值替換為右兄弟的第一個元素(右兄弟失去了一個節點但仍然擁有最小數量的元素)
- 樹又重新平衡
- 否則,如果缺少元素節點的左兄弟存在且擁有多余的元素,那么向右旋轉
- 將父節點的分隔值復制到缺少元素節點的第一個節點(分隔值被移下來;缺少元素的節點現在有最小數量的元素)
- 將父節點的分隔值替換為左兄弟的最后一個元素(左兄弟失去了一個節點但仍然擁有最小數量的元素)
- 樹又重新平衡
- 否則,如果它的兩個直接兄弟節點都只有最小數量的元素,那么將它與一個直接兄弟節點以及父節點中它們的分隔值合并
- 將分隔值復制到左邊的節點(左邊的節點可以是缺少元素的節點或者擁有最小數量元素的兄弟節點)
- 將右邊節點中所有的元素移動到左邊節點(左邊節點現在擁有最大數量的元素,右邊節點為空)
- 將父節點中的分隔值和空的右子樹移除(父節點失去了一個元素)
- 如果父節點是根節點并且沒有元素了,那么釋放它并且讓合并之后的節點成為新的根節點(樹的深度減小)
- 否則,如果父節點的元素數量小于最小值,重新平衡父節點
先有4階B樹:
刪除22,分層查詢鍵值后,在葉子節點中直接刪除:
刪除3,父節點從右兒子那選擇最小值(或左兒子那選擇最大值),為新的鍵值(分隔值):
刪除5,此時左兄弟只有一個鍵值1,沒有多余鍵值,借不了,所以需要合并,將父節點中的分隔值4加入到左兒子節點,右兒子剩下的所有元素合并到左兒子中,合并后父節點為鍵值數為0,小于規定的最小鍵值數1(?m/2?-1),對父節點遞歸算法
在新一輪的算法中,對剛才失衡的父節點進行左旋轉:
三. B+樹的定義
3.1.B+樹的定義
B+樹可以視為B樹的一個變種,不同的是B+樹內部節點不保存數據,非葉結點中的每個引索項只含有對應子樹的最大關鍵字和指向子樹的指針,不含有該關鍵字對應記錄的存儲地址,例如關系型數據庫Mysql。
一顆m階的B+樹需要滿足下列條件:
- 每個分支結點最多包含m棵子樹
- 非根結點至少有兩棵子樹,其他每個分支結點至少含有?m/2?\lceil m/2 \rceil?m/2?棵子樹
- 如果根節點不是葉子節點,那么它至少有兩個子節點
- 結點的子樹個數與關鍵字個數相同,所有分支結點中僅包含它的子結點中關鍵字最大值以及指向該子結點的指針
- 所有葉結點包含全部關鍵字以及指向相應記錄的指針,葉結點中將關鍵字按大小順序排列,并且相鄰葉結點按大小順序相互鏈接起來
3.2.B+樹 VS B樹
B+樹相對于B樹的優勢有:
- 內部節點不保存數據,占用空間小,所以每個節點可以存儲的索引更多,即每個磁盤頁存儲的索引更多,樹的高度更低,IO次數更低,磁盤查詢效率更高
- 每次查詢都要到達最下面的葉子層才能拿到數據,性能更加穩定
- 所有數據都保存在葉子節點,所有葉子節點都順次連接,有利于遍歷操作
現在你知道為什么數據庫用B+樹而不用B樹了吧。
四. B+樹的操作
4.1.插入
如果節點超過節點中鍵值數的規定范圍則處于違規狀態
- 首先,查找要插入其中的節點的位置。接著把值插入這個節點中。
- 如果沒有節點處于違規狀態則處理結束。
- 如果某個節點有過多元素,則把它分裂為兩個節點,每個都有最小數目的元素,同時將分隔值復制到父節點中作為索引。
- 在樹上遞歸向上繼續這個處理直到到達根節點,如果根節點被分裂,則創建一個新根節點。
現有一個4階B+樹
插入2,分層遍歷索引,直接插入:
插入8,分層遍歷索引,節點已滿,于是分裂,將新的間隔值6復制到父節點中作為索引,產生兩個新的子節點:
4.2.刪除刪除
- 首先,在葉子節點中查找要刪除的值。接著從包含它的節點中刪除這個值。
- 如果沒有節點處于違規狀態則處理結束。
- 如果節點處于違規狀態則有兩種可能情況:
- 它的兄弟節點,就是同一個父節點的子節點,可以把一個或多個它的子節點轉移到當前節點,而把它返回為合法狀態,最后再使用從兄弟那借到的鍵值替代父節點中的分隔鍵值之后處理結束。
- 它的兄弟節點由于處在低邊界上而沒有額外的子節點。在這種情況下把兩個兄弟節點合并到一個單一的節點中,并刪除父節點中的分隔鍵值
- 如果刪除后的父節點鍵值數量不足,進行與B樹完全相同的刪除平衡操作
現有一4階B+樹:
刪除8,分層遍歷索引,直接刪除:
刪除2,找左兄弟借一個數據2,用借來的鍵值2替代父節點中初始的索引2:
刪除4,兄弟節點沒有數據可以借,于是與左兄弟節點合并,刪除父節點的索引4:
刪除5、6,刪除5時可以向右兄弟借到6,刪除6時就完全借不到了,只能合并,再刪除父節點中的索引
刪除7,兄弟節點借不到,合并后刪除父節點中的索引,父節點不符合規則,對父節點進行刪除后的平衡操作,父親節點的兄弟節點只有一個鍵值2,于是父節點與兄弟節點和父節點的索引3進行合并
紅黑樹傳遞門,沖沖沖
總結
以上是生活随笔為你收集整理的B树、B+树其实很简单,看不懂你找我的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM自动化的内存分配与内存回收
- 下一篇: Winpcap进行抓包,分析数据包结构并