最近重新學習MySQL,發現自己一直知道MySQL索引用到了B+樹,引發思考,為什么一定要是B+樹,其他樹或者其他數據結構不可以嗎?下文揭曉。
算法圖解網站,可以看到樹是怎么生成的
1. 二叉查找樹 (Binary Search Tree)
- 既然都是樹,就先從二叉查找樹開始吧。
BST的性質
- 二叉查找樹也稱為有序二叉查找樹,二叉查找樹具有以下性質:
- 任意節點左子樹不為空,則左子樹的值小于根節點的值
- 任意節點右子樹不為空,則右子樹的值大于根節點的值
- 任意節點的左右子樹也分別是二叉查找樹
-
沒有鍵值相等的節點
Binary Search Tree
- 上圖就為一個簡單的二叉查找樹,二叉查找樹的中序遍歷是有序的,從小到大輸出為:1, 3, 5, 8,9, 13
- 如果查找的鍵值為3,在二叉查找樹中,先比較根節點大小5>3, 找其左子樹,找到1,再比較1<3,找其右子樹,找到3。二叉查找樹找了2次,如果按照順序查找也是2次。
- 我們依次算下平均查找次數,二叉查找樹中查找次數為(1+2+2+3+3+3)/ 6 = 2.3次;如果順序查找,查找次數為(1+2+3+4+5+6) / 6 = 3.3 次。可以看到二叉查找樹的查找速度比順序查找更快。
BST的局限性及應用
-
一個二叉查找樹是由n個節點隨機構成的,在某些情況中,二叉查找樹會退化成一個有n個節點的線性鏈。如下圖
- 如上圖,如果我們的根節點選擇是最大或者最小的數,那么二叉查找樹就會退化成一個鏈,變成順序查找。
- 退化了的二叉查找樹查詢效率非常低,于是我們就想怎么才能最大性能的構造出一個二叉查找樹,使得這個二叉樹是平衡的(高度相差不大),這樣就引出了AVL樹(二叉平衡樹)
BST 的操作代價分析
(1) 查找代價:
- 任何一個數據的查找過程都需要從根結點出發,沿某一個路徑朝葉子結點前進。因此查找中數據比較次數與樹的形態密切相關。
- 當樹中每個結點左右子樹高度大致相同時,樹高為logN。則平均查找長度與logN成正比,查找的平均時間復雜度在O(logN)數量級上。
-
當先后插入的關鍵字有序時,BST退化成單支樹結構。此時樹高n。平均查找長度為(n+1)/2,查找的平均時間復雜度在O(N)數量級上。
(2) 插入代價:
新結點插入到樹的葉子上,完全不需要改變樹中原有結點的組織結構。插入一個結點的代價與查找一個不存在的數據的代價完全相同。
(3) 刪除代價:
- 當刪除一個結點P,首先需要定位到這個結點P,這個過程需要一個查找的代價。然后稍微改變一下樹的形態。
- 如果被刪除結點的左、右子樹只有一個存在,則改變形態的代價僅為O(1)。
- 如果被刪除結點的左、右子樹均存在,只需要將當P的左孩子的右孩子的右孩子的…的右葉子結點與P互換,在改變一些左右子樹即可。
- 因此刪除操作的時間復雜度最大不會超過O(logN)。
刪除節點左右子樹都在
時間復雜度分析
- 當樹中每個結點左右子樹高度大致相同時,樹高為logN。則平均查找長度與logN成正比,查找的平均時間復雜度是O(logN)
- 當先后插入的關鍵字有序時,BST退化成單支樹結構。此時樹高n。因此最差時間復雜度是O(N)。
- 插入刪除操作算法簡單,時間復雜度與查找差不多。
2. AVL樹(平衡二叉查找樹)
- 我們不能忍受二叉查找樹在最差的情況下(鏈式)的時間復雜度和順序查找的時間復雜度一致。因為當數據夠大的時候,左右子樹可能會有很大的高度差,為了防止這種情況,AVL樹橫空出世!
- AVL樹本質上就是帶有平衡條件的二叉查找樹
- AVL樹在左子樹與右子樹的高度差大于1的時候進行調整。
- 上圖是一個二叉查找樹通過左旋變成AVL樹的過程,通過上面的gif圖可以看出,任意節點的左右子樹的平衡因子的差值都不會大于1。
AVL 的操作代價分析
- 我們還是使用[1, 3, 5, 8,9, 13] 這6個節點
[站外圖片上傳中...(image-6e0bd7-1597541595431)]
(1) 查找代價:
AVL是嚴格平衡的BST(平衡因子不超過1)。那么查找過程與BST一樣,只是AVL不會出現最差情況的BST(單支樹)。因此查找效率最好,最壞情況都是O(logN)數量級的。
(2) 插入代價:
- AVL必須要保證嚴格平衡(|bf|<=1),那么每一次插入數據使得AVL中某些結點的平衡因子超過1就必須進行旋轉操作。
- 事實上,AVL的每一次插入結點操作最多只需要旋轉1次(單旋轉或雙旋轉)。
- 因此,總體上插入操作的代價仍然在O(logN)級別上(插入結點需要首先查找插入的位置)。
[站外圖片上傳中...(image-fc03e8-1597541595431)]
(3) 刪除代價:
- AVL刪除結點的算法可以參見BST的刪除結點,但是刪除之后必須檢查從刪除結點開始到根結點路徑上的所有結點的平衡因子。
- 因此刪除的代價稍微要大一些。每一次刪除操作最多需要O(logN)次旋轉。
- 因此,刪除操作的時間復雜度為O(logN)+O(logN)=O(2logN)
[站外圖片上傳中...(image-d72429-1597541595431)]
時間復雜度分析
- 查找的時間復雜度維持在O(logN),不會出現最差情況
- AVL樹在執行每個插入操作時最多需要1次旋轉,其時間復雜度在O(logN)左右。
- AVL樹在執行刪除時代價稍大,執行每個刪除操作的時間復雜度需要O(2logN)。
3. 紅黑樹
- 二叉平衡樹的嚴格平衡策略以犧牲建立查找結構(插入,刪除操作)的代價,換來了穩定的O(logN) 的查找時間復雜度。但是這樣做是否值得呢?
- 能不能找一種折中策略,即不犧牲太大的建立查找結構的代價,也能保證穩定高效的查找效率呢? 答案就是:紅黑樹。
紅黑樹簡介
- 紅黑樹是一種二叉查找樹,但是在每個節點增加一個存儲位表示節點的顏色,共有兩種顏色,紅色和黑色。
- 通過對任何一條從根到葉子的路徑上的各個節點著色的方式,確保沒有一條路徑會比其他路徑長出兩倍。
- 紅黑樹是一種弱平衡二叉樹(由于是弱平衡,在相同的節點的情況下,AVL的高度低于紅黑樹),相對于要求平衡嚴格的AVL樹來說,紅黑樹旋轉的次數更少。
- 在對于搜索、插入、刪除操作多得情況下,我們就用紅黑樹。
紅黑樹性質
- 每個節點非紅即黑
- 根節點必須是黑的
- 每個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的;
- 如果一個節點是紅的,那么它的兩兒子都是黑的;
- 對于任意節點而言,其到葉子點樹NULL指針的每條路徑都包含相同數目的黑節點;
- 每條路徑都包含相同的黑節點;
性質3中指定紅黑樹的每個葉子節點都是空節點,而且葉子節點都是黑色。但是Java實現的紅黑樹將使用null來代表空節點,因此遍歷紅黑樹時將看不到黑色的葉子節點,可以看到葉子節點有紅色的
性質4的意思是從根到節點的路徑上不會有兩個連續的紅色節點,但是黑色節點是可以連續的。因此如果給定黑色節點的個數為N,最短路徑的情況是連續的N個黑色,樹的高度為N-1;最長路徑的情況為節點紅黑相同,樹的高度為2(N-1)
性質5是成為紅黑樹的最主要的條件,后序的插入、刪除操作都是為了遵守這個規定。
-
我們還是使用[1, 3, 5, 8,9, 13] 這6個節點構造紅黑樹
構造紅黑樹
(1) 查找代價:
- 由于紅黑樹的性質(最長路徑長度不超過最短路徑長度的2倍),可以說明紅黑樹雖然不像AVL一樣是嚴格平衡的,但平衡性能還是要比BST要好。
-
其查找代價基本維持在O(logN)左右,但在最差情況下(最長路徑是最短路徑的2倍少1),比AVL要略遜色一點。
RBT查找節點
(2) 插入代價:
- RBT插入結點時,需要旋轉操作和變色操作。* 但由于只需要保證RBT基本平衡就可以了。因此插入結點最多只需要2次旋轉,這一點和AVL的插入操作一樣。
-
雖然變色操作需要O(logN),但是變色操作十分簡單,代價很小。
RBT插入節點
(3) 刪除代價:
RBT的刪除操作代價要比AVL要好的多,刪除一個結點最多只需要3次旋轉操作。
RBT 效率總結 :
- 查找 效率最好情況下時間復雜度為O(logN),但在最壞情況下比AVL要差一些,但也遠遠好于BST。
- 插入和刪除操作改變樹的平衡性的概率要遠遠小于AVL(RBT不是高度平衡的)。因此需要的旋轉操作的可能性要小,而且一旦需要旋轉,插入一個結點最多只需要旋轉2次,刪除最多只需要旋轉3次(小于AVL的刪除操作所需要的旋轉次數)。
- 雖然變色操作的時間復雜度在O(logN),但是實際上,這種操作由于簡單所需要的代價很小。
紅黑樹的應用
- 廣泛用于C++的STL中,Map和Set都是用紅黑樹實現的;
- 著名的Linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊,進程的虛擬內存區域都存儲在一顆紅黑樹上,每個虛擬地址區域都對應紅黑樹的一個節點,左指針指向相鄰的地址虛擬存儲區域,右指針指向相鄰的高地址虛擬地址空間;
- IO多路復用epoll的實現采用紅黑樹組織管理sockfd,以支持快速的增刪改查;
- Nginx中用紅黑樹管理timer,因為紅黑樹是有序的,可以很快的得到距離當前最小的定時器;
- Java中TreeMap的實現;
紅黑樹為什么是在內存中使用的數據結構?
- 為什么數據庫中不使用紅黑樹進行查找呢?
- 將大量數據全部放入內存組織成RBT結構顯然是不實際的。實際上,像OS中的文件目錄存儲,數據庫中的文件索引結構的存儲…. 都不可能在內存中建立查找結構。數據必須在磁盤中建立好這個結構。
- 這就涉及到磁盤的存儲原理了,操作系統讀寫磁盤的基本單位是扇區,而文件系統的基本單位是簇(Cluster)(每個簇或者塊可以包括2、4、8、16、32、64…2的n次方個扇區。)。
-
意思就是,磁盤讀寫有一個最少內容的限制,即使我們只需要這個簇上的一個字節,我們也必須把整個簇的內容都讀完。
- 那么現在就有一個悲催的事情了,
- 如果一個父節點只有2個子結點,并不能填滿一個簇上的所有內容,那多余的地方就浪費了,考慮到磁盤的存儲原理,B/B+樹應運而生了。
- 由于B/B+樹分支比二叉樹多,所以相同數量的內容,B+樹的深度更淺。B+樹的深度就代表了磁盤的 I/O 次數。
- 數據庫設計的時候B+樹有多少個分支都是按照磁盤上一個簇最多能放多少節點設計的,
- 因此一般來說,涉及到磁盤上查詢的數據結構,都是使用B/B+樹
[圖片上傳失敗...(image-573a3a-1597541595431)]
4. B樹
- 先來看看B樹,B樹也稱B-樹,是一棵多路平衡查找樹,我們描述一棵B樹時需要指定他的階數。階數表示一個結點最多有多少個孩子結點。一般我們用字母m表示階數,當m取2時,就是我們常見的二叉樹。
B樹的性質
- 一棵m階的二叉樹定義如下:
- 每個節點最多有m-1個關鍵字
- 根節點最少可以只有1個關鍵字
- 非根節點至少有math.ceil(m/2) - 1 個關鍵字 (math.ceil表示向上取整)
- 每個結點中的關鍵字都按照從小到大的順序排列,每個關鍵字的左子樹中的所有關鍵字都小于它,而右子樹中的所有關鍵字都大于它。
- 所有葉子結點都位于同一層(或者說根節點到每個葉子節點的長度都相同)
- 上圖是一顆階數為4的B樹,實際應用中的B樹的階數m都非常大(通常大于100),所以即使存儲大量的數據,B樹的高度仍然比較小。
- B樹中的每個結點中存儲了關鍵字(key)和關鍵字對應的數據(data),以及孩子結點的指針。
- 我們將一個key和其對應的data稱為一個記錄。
- 但為了方便描述,除非特別說明,后續文中就用key來代替(key, value)鍵值對這個整體。
- 在數據庫中我們將B樹(和B+樹)作為索引結構,可以加快查詢速度,此時B樹中的key就表示鍵,而data表示了這個鍵對應的條目在硬盤上的邏輯地址。
B樹的查找操作
- B-Tree作為一個平衡多路查找樹(m-叉)。B樹的查找分成兩種:
- 一種是從一個結點查找另一結點的地址的時候,需要定位磁盤地址(查找地址),查找代價極高。
- 另一種是將結點中的有序關鍵字序列放入內存,進行優化查找(可以用折半),相比查找代價極低。而B樹的高度很小,因此在這一背景下,B樹比任何二叉結構查找樹的效率都要高很多。而且B+樹作為B樹的變種,其查找效率更高。
B樹的插入操作
插入操作是指插入一條記錄,即(key, value)的鍵值對。
如果B樹中已存在需要插入的鍵值對,則用需要插入的value替換舊的value。若B樹不存在這個key,則一定是在葉子結點中進行插入操作。
B樹的插入步驟:
- 根據要插入的key的值,找到葉子結點并插入。
- 判斷當前結點key的個數是否小于等于m-1,若滿足則結束,否則進行第3步。
- 以結點中間的key為中心分裂成左右兩部分,然后將這個中間的key插入到父結點中,這個key的左子樹指向分裂后的左半部分,這個key的右子支指向分裂后的右半部分,然后將當前結點指向父結點,繼續進行第3步。
下面以5階B樹為例,介紹B樹的插入操作,在5階B樹中,結點最多有4個key,最少有2個key
a)在空樹中插入39
此時根結點就一個key,此時根結點也是葉子結點
b)繼續插入22,97和41
根結點此時有4個key
c)繼續插入53
- 插入后超過了最大允許的關鍵字個數4,所以以key值為41為中心進行分裂,
- 結果如下圖所示,分裂后當前結點指針指向父結點,滿足B樹條件,插入操作結束。
- 當階數m為偶數時,需要分裂時就不存在排序恰好在中間的key,那么我們選擇中間位置的前一個key或中間位置的后一個key為中心進行分裂即可。
d)依次插入13,21,40,同樣會造成分裂,結果如下圖所示。
e)依次插入30,27, 33 ;36,35,34 ;24,29,結果如下圖所示。
f)插入key值為26的記錄,插入后的結果如下圖所示。
當前結點需要以27為中心分裂,并向父結點進位27,然后當前結點指向父結點,結果如下圖所示。
進位后導致當前結點(即根結點)也需要分裂,分裂的結果如下圖所示。
分裂后當前結點指向新的根,此時無需調整。
g)最后再依次插入key為17,28,29,31,32的記錄,結果如下圖所示。
在實現B樹的代碼中,為了使代碼編寫更加容易,我們可以將結點中存儲記錄的數組長度定義為m而非m-1,這樣方便底層的結點由于分裂向上層插入一個記錄時,上層有多余的位置存儲這個記錄。
同時,每個結點還可以存儲它的父結點的引用,這樣就不必編寫遞歸程序。
一般來說,對于確定的m和確定類型的記錄,結點大小是固定的,無論它實際存儲了多少個記錄。
但是分配固定結點大小的方法會存在浪費的情況,比如key為28,29所在的結點,還有2個key的位置沒有使用,但是已經不可能繼續在插入任何值了,因為這個結點的前序key是27,后繼key是30,所有整數值都用完了。
所以如果記錄先按key的大小排好序,再插入到B樹中,結點的使用率就會很低,最差情況下使用率僅為50%。
B樹的刪除操作
- 刪除操作是指,根據key刪除記錄,如果B樹中的記錄中不存對應key的記錄,則刪除失敗。
如果當前需要刪除的key位于非葉子結點上,則用后繼key(這里的后繼key均指后繼記錄的意思)覆蓋要刪除的key,然后在后繼key所在的子支中刪除該后繼key。此時后繼key一定位于葉子結點上,這個過程和二叉搜索樹刪除結點的方式類似。刪除這個記錄后執行第2步
該結點key個數大于等于Math.ceil(m/2)-1,結束刪除操作,否則執行第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,結果如下圖。
當刪除后,當前結點中只有1個key,而兄弟結點中也僅有2個key。所以只能讓父結點中的30下移和這個兩個孩子結點中的key合并,成為一個新的結點,當前結點的指針指向父結點。結果如下圖所示。
當前結點key的個數滿足條件,故刪除結束。
e)上述情況下,我們接著刪除key為40的記錄,刪除后結果如下圖所示。
同理,當前結點的記錄數小于2,兄弟結點中沒有多余key,所以父結點中的key下移,和兄弟(這里我們選擇左兄弟,選擇右兄弟也可以)結點合并,合并后的指向當前結點的指針就指向了父結點。
同理,對于當前結點而言只能繼續合并了,最后結果如下所示。
合并后結點當前結點滿足條件,刪除結束。
終于講完了B樹的操作,也知道為什么數據庫索引使用B樹而不用紅黑樹了
但是!!!我們還想更優化一點
B樹的每個節點都有data域(指針),這個操作就會增大節點的大小,說白了也就是會增加磁盤I/O次數(磁盤I/O一次讀出的數據量大小是固定的,單個數據變大,每次讀出的就少,IO次數增多,IO多耗時就長啊朋友!)
那我們是不是可以出了葉子節點外,其他節點不存儲數據,節點小,磁盤IO次數就少。
我們還可以將所有在葉子節點的Data域用指針連接成鏈,這樣遍歷葉子節點就能獲得全部的數據,這樣也能進行順序查找(支持區間訪問),遍歷效率也會提高。
在數據庫中基于范圍的查詢是非常頻繁的,如果使用B樹,效率會非常低。
5. B+樹
- B+樹定義:關鍵字個數比孩子結點個數小1,這種方式是和B樹基本等價的。下面是一顆階數為4的B+樹。
除此之外B+樹還有以下的要求。
B+樹包含2種類型的結點:內部結點(也稱索引結點)和葉子結點。根結點本身即可以是內部結點,也可以是葉子結點。根結點的關鍵字個數最少可以只有1個。
B+樹與B樹最大的不同是內部結點不保存數據,只用于索引,所有數據(或者說記錄)都保存在葉子結點中。
m階B+樹表示了內部結點最多有m-1個關鍵字(或者說內部結點最多有m個子樹),階數m同時限制了葉子結點最多存儲m-1個記錄。
內部結點中的key都按照從小到大的順序排列,對于內部結點中的一個key,左樹中的所有key都小于它,右子樹中的key都大于等于它。葉子結點中的記錄也按照key的大小排列。
5)每個葉子結點都存有相鄰葉子結點的指針,葉子結點本身依關鍵字的大小自小而大順序鏈接。
B+樹的插入操作
- 若為空樹,創建一個葉子結點,然后將記錄插入其中,此時這個葉子結點也是根結點,插入操作結束。
- 針對葉子類型結點:根據key值找到葉子結點,向這個葉子結點插入記錄。插入后,若當前結點key的個數小于等于m-1,則插入結束。否則將這個葉子結點分裂成左右兩個葉子結點,左葉子結點包含前m/2個記錄,右結點包含剩下的記錄,將第m/2+1個記錄的key進位到父結點中(父結點一定是索引類型結點),進位到父結點的key左孩子指針向左結點,右孩子指針向右結點。將當前結點的指針指向父結點,然后執行第3步。
- 針對索引類型結點:若當前結點key的個數小于等于m-1,則插入結束。否則,將這個索引類型結點分裂成兩個索引結點,左索引結點包含前(m-1)/2個key,右結點包含m-(m-1)/2個key,將第m/2個key進位到父結點中,進位到父結點的key左孩子指向左結點, 進位到父結點的key右孩子指向右結點。將當前結點的指針指向父結點,然后重復第3步。
下面是一顆5階B樹的插入過程,5階B數的結點最少2個key,最多4個key。
a)空樹中插入5
b)依次插入8,10,15
c)插入16
插入16后超過了關鍵字的個數限制,所以要進行分裂。在葉子結點分裂時,分裂出來的左結點2個記錄,右邊3個記錄,中間key成為索引結點中的key,分裂后當前結點指向了父結點(根結點)。結果如下圖所示。
當然我們還有另一種分裂方式,給左結點3個記錄,右結點2個記錄,此時索引結點中的key就變為15。
d)插入17
e)插入18,插入后如下圖所示
當前結點的關鍵字個數大于5,進行分裂。分裂成兩個結點,左結點2個記錄,右結點3個記錄,關鍵字16進位到父結點(索引類型)中,將當前結點的指針指向父結點。
當前結點的關鍵字個數滿足條件,插入結束。
f)插入若干數據后
g)在上圖中插入7,結果如下圖所示
當前結點的關鍵字個數超過4,需要分裂。左結點2個記錄,右結點3個記錄。分裂后關鍵字7進入到父結點中,將當前結點的指針指向父結點,結果如下圖所示。
當前結點的關鍵字個數超過4,需要繼續分裂。左結點2個關鍵字,右結點2個關鍵字,關鍵字16進入到父結點中,將當前結點指向父結點,結果如下圖所示。
當前結點的關鍵字個數滿足條件,插入結束。
B+樹的刪除操作
如果葉子結點中沒有相應的key,則刪除失敗。否則執行下面的步驟
刪除葉子結點中對應的key。刪除后若結點的key的個數大于等于Math.ceil(m-1)/2 – 1,刪除操作結束,否則執行第2步。
若兄弟結點key有富余(大于Math.ceil(m-1)/2 – 1),向兄弟結點借一個記錄,同時用借到的key替換父結(指當前結點和兄弟結點共同的父結點)點中的key,刪除結束。否則執行第3步。
若兄弟結點中沒有富余的key,則當前結點和兄弟結點合并成一個新的葉子結點,并刪除父結點中的key(父結點中的這個key兩邊的孩子指針就變成了一個指針,正好指向這個新的葉子結點),將當前結點指向父結點(必為索引結點),執行第4步(第4步以后的操作和B樹就完全一樣了,主要是為了更新索引結點)。
若索引結點的key的個數大于等于Math.ceil(m-1)/2 – 1,則刪除操作結束。否則執行第5步
若兄弟結點有富余,父結點key下移,兄弟結點key上移,刪除結束。否則執行第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+樹性能分析
- B+樹中一次檢索最多需要h-1次IO,(根節點常駐內存),漸進復雜度為O(h) = O(logmN)。
- 實際應用中,m是非常大的數字,通常超過100,因此h非常小(通常不超過3)
- 意思就是,用B+樹作為索引結構效率是非常高的。紅黑樹的結構,h明顯非常深,查詢效率會低很多
為什么說B+樹比B樹更適合數據庫索引?
1、 B+樹的磁盤讀寫代價更低:B+樹的內部節點并沒有指向關鍵字具體信息的指針,因此其內部節點相對B樹更小,如果把所有同一內部節點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多,一次性讀入內存的需要查找的關鍵字也就越多,相對IO讀寫次數就降低了。
2、B+樹的查詢效率更加穩定:由于非終結點并不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。
3、由于B+樹的數據都存儲在葉子結點中,分支結點均為索引,方便掃庫,只需要掃一遍葉子結點即可,但是B樹因為其分支結點同樣存儲著數據,我們要找到具體的數據,需要進行一次中序遍歷按序來掃,所以B+樹更加適合在區間查詢的情況,所以通常B+樹用于數據庫索引。
PS:我在知乎上看到有人是這樣說的,我感覺說的也挺有道理的:
他們認為數據庫索引采用B+樹的主要原因是:B樹在提高了IO性能的同時并沒有解決元素遍歷的我效率低下的問題,正是為了解決這個問題,B+樹應用而生。B+樹只需要去遍歷葉子節點就可以實現整棵樹的遍歷。而且在數據庫中基于范圍的查詢是非常頻繁的,而B樹不支持這樣的操作或者說效率太低。