深入思考MySQL索引底層為什么用到B+樹,為什么不用平衡樹、紅黑樹、二叉樹、B樹、map等結(jié)構(gòu)?

最近重新學習MySQL,發(fā)現(xiàn)自己一直知道MySQL索引用到了B+樹,引發(fā)思考,為什么一定要是B+樹,其他樹或者其他數(shù)據(jù)結(jié)構(gòu)不可以嗎?下文揭曉。
算法圖解網(wǎng)站,可以看到樹是怎么生成的

1. 二叉查找樹 (Binary Search Tree)

  • 既然都是樹,就先從二叉查找樹開始吧。

BST的性質(zhì)

  • 二叉查找樹也稱為有序二叉查找樹,二叉查找樹具有以下性質(zhì):
  1. 任意節(jié)點左子樹不為空,則左子樹的值小于根節(jié)點的值
  2. 任意節(jié)點右子樹不為空,則右子樹的值大于根節(jié)點的值
  3. 任意節(jié)點的左右子樹也分別是二叉查找樹
  4. 沒有鍵值相等的節(jié)點


    Binary Search Tree
  • 上圖就為一個簡單的二叉查找樹,二叉查找樹的中序遍歷是有序的,從小到大輸出為:1, 3, 5, 8,9, 13
  • 如果查找的鍵值為3,在二叉查找樹中,先比較根節(jié)點大小5>3, 找其左子樹,找到1,再比較1<3,找其右子樹,找到3。二叉查找樹找了2次,如果按照順序查找也是2次。
  • 我們依次算下平均查找次數(shù),二叉查找樹中查找次數(shù)為(1+2+2+3+3+3)/ 6 = 2.3次;如果順序查找,查找次數(shù)為(1+2+3+4+5+6) / 6 = 3.3 次。可以看到二叉查找樹的查找速度比順序查找更快

BST的局限性及應(yīng)用

  • 一個二叉查找樹是由n個節(jié)點隨機構(gòu)成的,在某些情況中,二叉查找樹會退化成一個有n個節(jié)點的線性鏈。如下圖


  • 如上圖,如果我們的根節(jié)點選擇是最大或者最小的數(shù),那么二叉查找樹就會退化成一個鏈,變成順序查找。
  • 退化了的二叉查找樹查詢效率非常低,于是我們就想怎么才能最大性能的構(gòu)造出一個二叉查找樹,使得這個二叉樹是平衡的(高度相差不大),這樣就引出了AVL樹(二叉平衡樹)

BST 的操作代價分析

(1) 查找代價:

  • 任何一個數(shù)據(jù)的查找過程都需要從根結(jié)點出發(fā),沿某一個路徑朝葉子結(jié)點前進。因此查找中數(shù)據(jù)比較次數(shù)與樹的形態(tài)密切相關(guān)。
  • 當樹中每個結(jié)點左右子樹高度大致相同時,樹高為logN。則平均查找長度與logN成正比,查找的平均時間復(fù)雜度在O(logN)數(shù)量級上。
  • 當先后插入的關(guān)鍵字有序時,BST退化成單支樹結(jié)構(gòu)。此時樹高n。平均查找長度為(n+1)/2,查找的平均時間復(fù)雜度在O(N)數(shù)量級上。


(2) 插入代價:

新結(jié)點插入到樹的葉子上,完全不需要改變樹中原有結(jié)點的組織結(jié)構(gòu)。插入一個結(jié)點的代價與查找一個不存在的數(shù)據(jù)的代價完全相同。


(3) 刪除代價:

  • 當刪除一個結(jié)點P,首先需要定位到這個結(jié)點P,這個過程需要一個查找的代價。然后稍微改變一下樹的形態(tài)。
  • 如果被刪除結(jié)點的左、右子樹只有一個存在,則改變形態(tài)的代價僅為O(1)。
  • 如果被刪除結(jié)點的左、右子樹均存在,只需要將當P的左孩子的右孩子的右孩子的…的右葉子結(jié)點與P互換,在改變一些左右子樹即可。
  • 因此刪除操作的時間復(fù)雜度最大不會超過O(logN)
    刪除節(jié)點左右子樹都在

時間復(fù)雜度分析

  • 當樹中每個結(jié)點左右子樹高度大致相同時,樹高為logN。則平均查找長度與logN成正比,查找的平均時間復(fù)雜度是O(logN)
  • 當先后插入的關(guān)鍵字有序時,BST退化成單支樹結(jié)構(gòu)。此時樹高n。因此最差時間復(fù)雜度是O(N)
  • 插入刪除操作算法簡單,時間復(fù)雜度與查找差不多。

2. AVL樹(平衡二叉查找樹)

  • 我們不能忍受二叉查找樹在最差的情況下(鏈式)的時間復(fù)雜度和順序查找的時間復(fù)雜度一致。因為當數(shù)據(jù)夠大的時候,左右子樹可能會有很大的高度差,為了防止這種情況,AVL樹橫空出世!
  • AVL樹本質(zhì)上就是帶有平衡條件的二叉查找樹
  • AVL樹在左子樹與右子樹的高度差大于1的時候進行調(diào)整。
二叉查找樹
二叉平衡樹
  • 上圖是一個二叉查找樹通過左旋變成AVL樹的過程,通過上面的gif圖可以看出,任意節(jié)點的左右子樹的平衡因子的差值都不會大于1。

AVL 的操作代價分析

  • 我們還是使用[1, 3, 5, 8,9, 13] 這6個節(jié)點
    [站外圖片上傳中...(image-6e0bd7-1597541595431)]

(1) 查找代價:

AVL是嚴格平衡的BST(平衡因子不超過1)。那么查找過程與BST一樣,只是AVL不會出現(xiàn)最差情況的BST(單支樹)。因此查找效率最好,最壞情況都是O(logN)數(shù)量級的。


AVL查找節(jié)點

(2) 插入代價:

  • AVL必須要保證嚴格平衡(|bf|<=1),那么每一次插入數(shù)據(jù)使得AVL中某些結(jié)點的平衡因子超過1就必須進行旋轉(zhuǎn)操作。
  • 事實上,AVL的每一次插入結(jié)點操作最多只需要旋轉(zhuǎn)1次(單旋轉(zhuǎn)或雙旋轉(zhuǎn))
  • 因此,總體上插入操作的代價仍然在O(logN)級別上(插入結(jié)點需要首先查找插入的位置)。
    [站外圖片上傳中...(image-fc03e8-1597541595431)]

(3) 刪除代價:

  • AVL刪除結(jié)點的算法可以參見BST的刪除結(jié)點,但是刪除之后必須檢查從刪除結(jié)點開始到根結(jié)點路徑上的所有結(jié)點的平衡因子。
  • 因此刪除的代價稍微要大一些。每一次刪除操作最多需要O(logN)次旋轉(zhuǎn)。
  • 因此,刪除操作的時間復(fù)雜度為O(logN)+O(logN)=O(2logN)
    [站外圖片上傳中...(image-d72429-1597541595431)]

時間復(fù)雜度分析

  • 查找的時間復(fù)雜度維持在O(logN),不會出現(xiàn)最差情況
  • AVL樹在執(zhí)行每個插入操作時最多需要1次旋轉(zhuǎn),其時間復(fù)雜度在O(logN)左右。
  • AVL樹在執(zhí)行刪除時代價稍大,執(zhí)行每個刪除操作的時間復(fù)雜度需要O(2logN)。

3. 紅黑樹

  • 二叉平衡樹的嚴格平衡策略以犧牲建立查找結(jié)構(gòu)(插入,刪除操作)的代價,換來了穩(wěn)定的O(logN) 的查找時間復(fù)雜度。但是這樣做是否值得呢?
  • 能不能找一種折中策略,即不犧牲太大的建立查找結(jié)構(gòu)的代價,也能保證穩(wěn)定高效的查找效率呢? 答案就是:紅黑樹。

紅黑樹簡介

  • 紅黑樹是一種二叉查找樹,但是在每個節(jié)點增加一個存儲位表示節(jié)點的顏色,共有兩種顏色,紅色和黑色。
  • 通過對任何一條從根到葉子的路徑上的各個節(jié)點著色的方式,確保沒有一條路徑會比其他路徑長出兩倍。
  • 紅黑樹是一種弱平衡二叉樹(由于是弱平衡,在相同的節(jié)點的情況下,AVL的高度低于紅黑樹),相對于要求平衡嚴格的AVL樹來說,紅黑樹旋轉(zhuǎn)的次數(shù)更少。
  • 在對于搜索、插入、刪除操作多得情況下,我們就用紅黑樹。

紅黑樹性質(zhì)

  1. 每個節(jié)點非紅即黑
  2. 根節(jié)點必須是黑的
  3. 每個葉節(jié)點(葉節(jié)點即樹尾端NULL指針或NULL節(jié)點)都是黑的;
  4. 如果一個節(jié)點是紅的,那么它的兩兒子都是黑的;
  5. 對于任意節(jié)點而言,其到葉子點樹NULL指針的每條路徑都包含相同數(shù)目的黑節(jié)點;
  6. 每條路徑都包含相同的黑節(jié)點;
  • 性質(zhì)3中指定紅黑樹的每個葉子節(jié)點都是空節(jié)點,而且葉子節(jié)點都是黑色。但是Java實現(xiàn)的紅黑樹將使用null來代表空節(jié)點,因此遍歷紅黑樹時將看不到黑色的葉子節(jié)點,可以看到葉子節(jié)點有紅色的

  • 性質(zhì)4的意思是從根到節(jié)點的路徑上不會有兩個連續(xù)的紅色節(jié)點,但是黑色節(jié)點是可以連續(xù)的。因此如果給定黑色節(jié)點的個數(shù)為N,最短路徑的情況是連續(xù)的N個黑色,樹的高度為N-1;最長路徑的情況為節(jié)點紅黑相同,樹的高度為2(N-1)

  • 性質(zhì)5是成為紅黑樹的最主要的條件,后序的插入、刪除操作都是為了遵守這個規(guī)定。

紅黑樹性質(zhì)
  • 我們還是使用[1, 3, 5, 8,9, 13] 這6個節(jié)點構(gòu)造紅黑樹


    構(gòu)造紅黑樹

(1) 查找代價:

  • 由于紅黑樹的性質(zhì)(最長路徑長度不超過最短路徑長度的2倍),可以說明紅黑樹雖然不像AVL一樣是嚴格平衡的,但平衡性能還是要比BST要好。
  • 其查找代價基本維持在O(logN)左右,但在最差情況下(最長路徑是最短路徑的2倍少1),比AVL要略遜色一點。


    RBT查找節(jié)點

(2) 插入代價:

  • RBT插入結(jié)點時,需要旋轉(zhuǎn)操作和變色操作。* 但由于只需要保證RBT基本平衡就可以了。因此插入結(jié)點最多只需要2次旋轉(zhuǎn),這一點和AVL的插入操作一樣。
  • 雖然變色操作需要O(logN),但是變色操作十分簡單,代價很小。


    RBT插入節(jié)點

(3) 刪除代價:

RBT的刪除操作代價要比AVL要好的多,刪除一個結(jié)點最多只需要3次旋轉(zhuǎn)操作。


RBT刪除節(jié)點

RBT 效率總結(jié) :

  • 查找 效率最好情況下時間復(fù)雜度為O(logN),但在最壞情況下比AVL要差一些,但也遠遠好于BST。
  • 插入刪除操作改變樹的平衡性的概率要遠遠小于AVL(RBT不是高度平衡的)。因此需要的旋轉(zhuǎn)操作的可能性要小,而且一旦需要旋轉(zhuǎn),插入一個結(jié)點最多只需要旋轉(zhuǎn)2次,刪除最多只需要旋轉(zhuǎn)3次(小于AVL的刪除操作所需要的旋轉(zhuǎn)次數(shù))。
  • 雖然變色操作的時間復(fù)雜度在O(logN),但是實際上,這種操作由于簡單所需要的代價很小。

紅黑樹的應(yīng)用

  1. 廣泛用于C++的STL中,Map和Set都是用紅黑樹實現(xiàn)的;
  2. 著名的Linux進程調(diào)度Completely Fair Scheduler,用紅黑樹管理進程控制塊,進程的虛擬內(nèi)存區(qū)域都存儲在一顆紅黑樹上,每個虛擬地址區(qū)域都對應(yīng)紅黑樹的一個節(jié)點,左指針指向相鄰的地址虛擬存儲區(qū)域,右指針指向相鄰的高地址虛擬地址空間;
  3. IO多路復(fù)用epoll的實現(xiàn)采用紅黑樹組織管理sockfd,以支持快速的增刪改查;
  4. Nginx中用紅黑樹管理timer,因為紅黑樹是有序的,可以很快的得到距離當前最小的定時器;
  5. Java中TreeMap的實現(xiàn);

紅黑樹為什么是在內(nèi)存中使用的數(shù)據(jù)結(jié)構(gòu)?

  • 為什么數(shù)據(jù)庫中不使用紅黑樹進行查找呢?
  • 將大量數(shù)據(jù)全部放入內(nèi)存組織成RBT結(jié)構(gòu)顯然是不實際的。實際上,像OS中的文件目錄存儲,數(shù)據(jù)庫中的文件索引結(jié)構(gòu)的存儲…. 都不可能在內(nèi)存中建立查找結(jié)構(gòu)。數(shù)據(jù)必須在磁盤中建立好這個結(jié)構(gòu)
  • 這就涉及到磁盤的存儲原理了,操作系統(tǒng)讀寫磁盤的基本單位是扇區(qū),而文件系統(tǒng)的基本單位是簇(Cluster)(每個簇或者塊可以包括2、4、8、16、32、64…2的n次方個扇區(qū)。)
  • 意思就是,磁盤讀寫有一個最少內(nèi)容的限制,即使我們只需要這個簇上的一個字節(jié),我們也必須把整個簇的內(nèi)容都讀完。


  • 那么現(xiàn)在就有一個悲催的事情了,
  • 如果一個父節(jié)點只有2個子結(jié)點,并不能填滿一個簇上的所有內(nèi)容,那多余的地方就浪費了,考慮到磁盤的存儲原理,B/B+樹應(yīng)運而生了。
  • 由于B/B+樹分支比二叉樹多,所以相同數(shù)量的內(nèi)容,B+樹的深度更淺。B+樹的深度就代表了磁盤的 I/O 次數(shù)
  • 數(shù)據(jù)庫設(shè)計的時候B+樹有多少個分支都是按照磁盤上一個簇最多能放多少節(jié)點設(shè)計的,
  • 因此一般來說,涉及到磁盤上查詢的數(shù)據(jù)結(jié)構(gòu),都是使用B/B+樹
    [圖片上傳失敗...(image-573a3a-1597541595431)]

4. B樹

  • 先來看看B樹,B樹也稱B-樹,是一棵多路平衡查找樹,我們描述一棵B樹時需要指定他的階數(shù)階數(shù)表示一個結(jié)點最多有多少個孩子結(jié)點。一般我們用字母m表示階數(shù),當m取2時,就是我們常見的二叉樹。

B樹的性質(zhì)

  • 一棵m階的二叉樹定義如下:
  1. 每個節(jié)點最多有m-1個關(guān)鍵字
  2. 根節(jié)點最少可以只有1個關(guān)鍵字
  3. 非根節(jié)點至少有math.ceil(m/2) - 1 個關(guān)鍵字 (math.ceil表示向上取整)
  4. 每個結(jié)點中的關(guān)鍵字都按照從小到大的順序排列,每個關(guān)鍵字的左子樹中的所有關(guān)鍵字都小于它,而右子樹中的所有關(guān)鍵字都大于它。
  5. 所有葉子結(jié)點都位于同一層(或者說根節(jié)點到每個葉子節(jié)點的長度都相同)
4階B樹
  • 上圖是一顆階數(shù)為4的B樹,實際應(yīng)用中的B樹的階數(shù)m都非常大(通常大于100),所以即使存儲大量的數(shù)據(jù),B樹的高度仍然比較小
  • B樹中的每個結(jié)點中存儲了關(guān)鍵字(key)和關(guān)鍵字對應(yīng)的數(shù)據(jù)(data),以及孩子結(jié)點的指針
  • 我們將一個key和其對應(yīng)的data稱為一個記錄。
  • 但為了方便描述,除非特別說明,后續(xù)文中就用key來代替(key, value)鍵值對這個整體。
  • 在數(shù)據(jù)庫中我們將B樹(和B+樹)作為索引結(jié)構(gòu),可以加快查詢速度,此時B樹中的key就表示鍵,而data表示了這個鍵對應(yīng)的條目在硬盤上的邏輯地址

B樹的查找操作

  • B-Tree作為一個平衡多路查找樹(m-叉)。B樹的查找分成兩種:
  1. 一種是從一個結(jié)點查找另一結(jié)點的地址的時候,需要定位磁盤地址(查找地址),查找代價極高
  2. 另一種是將結(jié)點中的有序關(guān)鍵字序列放入內(nèi)存,進行優(yōu)化查找(可以用折半),相比查找代價極低。而B樹的高度很小,因此在這一背景下,B樹比任何二叉結(jié)構(gòu)查找樹的效率都要高很多。而且B+樹作為B樹的變種,其查找效率更高。

B樹的插入操作

  • 插入操作是指插入一條記錄,即(key, value)的鍵值對。

  • 如果B樹中已存在需要插入的鍵值對,則用需要插入的value替換舊的value。若B樹不存在這個key,則一定是在葉子結(jié)點中進行插入操作。

  • B樹的插入步驟

  1. 根據(jù)要插入的key的值,找到葉子結(jié)點并插入。
  2. 判斷當前結(jié)點key的個數(shù)是否小于等于m-1,若滿足則結(jié)束,否則進行第3步。
  3. 以結(jié)點中間的key為中心分裂成左右兩部分,然后將這個中間的key插入到父結(jié)點中,這個key的左子樹指向分裂后的左半部分,這個key的右子支指向分裂后的右半部分,然后將當前結(jié)點指向父結(jié)點,繼續(xù)進行第3步。

下面以5階B樹為例,介紹B樹的插入操作,在5階B樹中,結(jié)點最多有4個key,最少有2個key


a)在空樹中插入39

此時根結(jié)點就一個key,此時根結(jié)點也是葉子結(jié)點


b)繼續(xù)插入22,97和41

根結(jié)點此時有4個key


c)繼續(xù)插入53


  • 插入后超過了最大允許的關(guān)鍵字個數(shù)4,所以以key值為41為中心進行分裂,
  • 結(jié)果如下圖所示,分裂后當前結(jié)點指針指向父結(jié)點,滿足B樹條件,插入操作結(jié)束。
  • 當階數(shù)m為偶數(shù)時,需要分裂時就不存在排序恰好在中間的key,那么我們選擇中間位置的前一個key或中間位置的后一個key為中心進行分裂即可。

d)依次插入13,21,40,同樣會造成分裂,結(jié)果如下圖所示。


e)依次插入30,27, 33 ;36,35,34 ;24,29,結(jié)果如下圖所示。


f)插入key值為26的記錄,插入后的結(jié)果如下圖所示。

當前結(jié)點需要以27為中心分裂,并向父結(jié)點進位27,然后當前結(jié)點指向父結(jié)點,結(jié)果如下圖所示。

進位后導(dǎo)致當前結(jié)點(即根結(jié)點)也需要分裂,分裂的結(jié)果如下圖所示。

分裂后當前結(jié)點指向新的根,此時無需調(diào)整。


g)最后再依次插入key為17,28,29,31,32的記錄,結(jié)果如下圖所示。


  • 在實現(xiàn)B樹的代碼中,為了使代碼編寫更加容易,我們可以將結(jié)點中存儲記錄的數(shù)組長度定義為m而非m-1,這樣方便底層的結(jié)點由于分裂向上層插入一個記錄時,上層有多余的位置存儲這個記錄。

  • 同時,每個結(jié)點還可以存儲它的父結(jié)點的引用,這樣就不必編寫遞歸程序。

  • 一般來說,對于確定的m和確定類型的記錄,結(jié)點大小是固定的,無論它實際存儲了多少個記錄。

  • 但是分配固定結(jié)點大小的方法會存在浪費的情況,比如key為28,29所在的結(jié)點,還有2個key的位置沒有使用,但是已經(jīng)不可能繼續(xù)在插入任何值了,因為這個結(jié)點的前序key是27,后繼key是30,所有整數(shù)值都用完了。

  • 所以如果記錄先按key的大小排好序,再插入到B樹中,結(jié)點的使用率就會很低,最差情況下使用率僅為50%。

B樹的刪除操作

  • 刪除操作是指,根據(jù)key刪除記錄,如果B樹中的記錄中不存對應(yīng)key的記錄,則刪除失敗。
  1. 如果當前需要刪除的key位于非葉子結(jié)點上,則用后繼key(這里的后繼key均指后繼記錄的意思)覆蓋要刪除的key,然后在后繼key所在的子支中刪除該后繼key。此時后繼key一定位于葉子結(jié)點上,這個過程和二叉搜索樹刪除結(jié)點的方式類似。刪除這個記錄后執(zhí)行第2步

  2. 該結(jié)點key個數(shù)大于等于Math.ceil(m/2)-1,結(jié)束刪除操作,否則執(zhí)行第3步。

  3. 如果兄弟結(jié)點key個數(shù)大于Math.ceil(m/2)-1,則父結(jié)點中的key下移到該結(jié)點,兄弟結(jié)點中的一個key上移,刪除操作結(jié)束

  4. 否則,將父結(jié)點中的key下移與當前結(jié)點及它的兄弟結(jié)點中的key合并,形成一個新的結(jié)點。原父結(jié)點中的key的兩個孩子指針就變成了一個孩子指針,指向這個新結(jié)點。然后當前結(jié)點的指針指向父結(jié)點,重復(fù)上第2步。

有些結(jié)點它可能即有左兄弟,又有右兄弟,那么我們?nèi)我膺x擇一個兄弟結(jié)點進行操作即可。

下面以5階B樹為例,介紹B樹的刪除操作,5階B樹中,結(jié)點最多有4個key,最少有2個key


a)原始狀態(tài)


b)在上面的B樹中刪除21,刪除后結(jié)點中的關(guān)鍵字個數(shù)仍然大于等2,所以刪除結(jié)束。


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

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


d)在上述情況下接著刪除32,結(jié)果如下圖。

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

當前結(jié)點key的個數(shù)滿足條件,故刪除結(jié)束。


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

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

同理,對于當前結(jié)點而言只能繼續(xù)合并了,最后結(jié)果如下所示。

合并后結(jié)點當前結(jié)點滿足條件,刪除結(jié)束。

  • 終于講完了B樹的操作,也知道為什么數(shù)據(jù)庫索引使用B樹而不用紅黑樹了

  • 但是!!!我們還想更優(yōu)化一點

  • B樹的每個節(jié)點都有data域(指針),這個操作就會增大節(jié)點的大小,說白了也就是會增加磁盤I/O次數(shù)(磁盤I/O一次讀出的數(shù)據(jù)量大小是固定的,單個數(shù)據(jù)變大,每次讀出的就少,IO次數(shù)增多,IO多耗時就長啊朋友!)

  • 那我們是不是可以出了葉子節(jié)點外,其他節(jié)點不存儲數(shù)據(jù),節(jié)點小,磁盤IO次數(shù)就少。

  • 我們還可以將所有在葉子節(jié)點的Data域用指針連接成鏈,這樣遍歷葉子節(jié)點就能獲得全部的數(shù)據(jù),這樣也能進行順序查找(支持區(qū)間訪問),遍歷效率也會提高。

  • 在數(shù)據(jù)庫中基于范圍的查詢是非常頻繁的,如果使用B樹,效率會非常低。

5. B+樹

  • B+樹定義:關(guān)鍵字個數(shù)比孩子結(jié)點個數(shù)小1,這種方式是和B樹基本等價的。下面是一顆階數(shù)為4的B+樹。

除此之外B+樹還有以下的要求。

  1. B+樹包含2種類型的結(jié)點:內(nèi)部結(jié)點(也稱索引結(jié)點)和葉子結(jié)點。根結(jié)點本身即可以是內(nèi)部結(jié)點,也可以是葉子結(jié)點。根結(jié)點的關(guān)鍵字個數(shù)最少可以只有1個。

  2. B+樹與B樹最大的不同是內(nèi)部結(jié)點不保存數(shù)據(jù),只用于索引,所有數(shù)據(jù)(或者說記錄)都保存在葉子結(jié)點中。

  3. m階B+樹表示了內(nèi)部結(jié)點最多有m-1個關(guān)鍵字(或者說內(nèi)部結(jié)點最多有m個子樹),階數(shù)m同時限制了葉子結(jié)點最多存儲m-1個記錄。

  4. 內(nèi)部結(jié)點中的key都按照從小到大的順序排列,對于內(nèi)部結(jié)點中的一個key,左樹中的所有key都小于它,右子樹中的key都大于等于它。葉子結(jié)點中的記錄也按照key的大小排列

5)每個葉子結(jié)點都存有相鄰葉子結(jié)點的指針,葉子結(jié)點本身依關(guān)鍵字的大小自小而大順序鏈接。

B+樹的插入操作

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

下面是一顆5階B樹的插入過程,5階B數(shù)的結(jié)點最少2個key,最多4個key。


a)空樹中插入5


b)依次插入8,10,15


c)插入16

插入16后超過了關(guān)鍵字的個數(shù)限制,所以要進行分裂。在葉子結(jié)點分裂時,分裂出來的左結(jié)點2個記錄,右邊3個記錄,中間key成為索引結(jié)點中的key,分裂后當前結(jié)點指向了父結(jié)點(根結(jié)點)。結(jié)果如下圖所示。

當然我們還有另一種分裂方式,給左結(jié)點3個記錄,右結(jié)點2個記錄,此時索引結(jié)點中的key就變?yōu)?5。


d)插入17


e)插入18,插入后如下圖所示

當前結(jié)點的關(guān)鍵字個數(shù)大于5,進行分裂。分裂成兩個結(jié)點,左結(jié)點2個記錄,右結(jié)點3個記錄,關(guān)鍵字16進位到父結(jié)點(索引類型)中,將當前結(jié)點的指針指向父結(jié)點。

當前結(jié)點的關(guān)鍵字個數(shù)滿足條件,插入結(jié)束。


f)插入若干數(shù)據(jù)后


g)在上圖中插入7,結(jié)果如下圖所示

當前結(jié)點的關(guān)鍵字個數(shù)超過4,需要分裂。左結(jié)點2個記錄,右結(jié)點3個記錄。分裂后關(guān)鍵字7進入到父結(jié)點中,將當前結(jié)點的指針指向父結(jié)點,結(jié)果如下圖所示。

當前結(jié)點的關(guān)鍵字個數(shù)超過4,需要繼續(xù)分裂。左結(jié)點2個關(guān)鍵字,右結(jié)點2個關(guān)鍵字,關(guān)鍵字16進入到父結(jié)點中,將當前結(jié)點指向父結(jié)點,結(jié)果如下圖所示。

當前結(jié)點的關(guān)鍵字個數(shù)滿足條件,插入結(jié)束。

B+樹的刪除操作

如果葉子結(jié)點中沒有相應(yīng)的key,則刪除失敗。否則執(zhí)行下面的步驟

  1. 刪除葉子結(jié)點中對應(yīng)的key。刪除后若結(jié)點的key的個數(shù)大于等于Math.ceil(m-1)/2 – 1,刪除操作結(jié)束,否則執(zhí)行第2步。

  2. 若兄弟結(jié)點key有富余(大于Math.ceil(m-1)/2 – 1),向兄弟結(jié)點借一個記錄,同時用借到的key替換父結(jié)(指當前結(jié)點和兄弟結(jié)點共同的父結(jié)點)點中的key,刪除結(jié)束。否則執(zhí)行第3步。

  3. 若兄弟結(jié)點中沒有富余的key,則當前結(jié)點和兄弟結(jié)點合并成一個新的葉子結(jié)點,并刪除父結(jié)點中的key(父結(jié)點中的這個key兩邊的孩子指針就變成了一個指針,正好指向這個新的葉子結(jié)點),將當前結(jié)點指向父結(jié)點(必為索引結(jié)點),執(zhí)行第4步(第4步以后的操作和B樹就完全一樣了,主要是為了更新索引結(jié)點)。

  4. 若索引結(jié)點的key的個數(shù)大于等于Math.ceil(m-1)/2 – 1,則刪除操作結(jié)束。否則執(zhí)行第5步

  5. 若兄弟結(jié)點有富余,父結(jié)點key下移,兄弟結(jié)點key上移,刪除結(jié)束。否則執(zhí)行第6步

  6. 當前結(jié)點和兄弟結(jié)點及父結(jié)點下移key合并成一個新的結(jié)點。將當前結(jié)點指向父結(jié)點,重復(fù)第4步。

注意,通過B+樹的刪除操作后,索引結(jié)點中存在的key,不一定在葉子結(jié)點中存在對應(yīng)的記錄

下面是一顆5階B樹的刪除過程,5階B數(shù)的結(jié)點最少2個key,最多4個key。


a)初始狀態(tài)


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

刪除后葉子結(jié)點中key的個數(shù)大于等于2,刪除結(jié)束


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

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


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

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

此時當前結(jié)點的關(guān)鍵字個數(shù)小于2,兄弟結(jié)點的關(guān)鍵字也沒有富余,所以父結(jié)點中的關(guān)鍵字下移,和兩個孩子結(jié)點合并,結(jié)果如下圖所示。

圖文引用

B+樹性能分析

  • B+樹中一次檢索最多需要h-1次IO,(根節(jié)點常駐內(nèi)存),漸進復(fù)雜度為O(h) = O(logmN)。
  • 實際應(yīng)用中,m是非常大的數(shù)字,通常超過100,因此h非常小(通常不超過3)
  • 意思就是,用B+樹作為索引結(jié)構(gòu)效率是非常高的。紅黑樹的結(jié)構(gòu),h明顯非常深,查詢效率會低很多

為什么說B+樹比B樹更適合數(shù)據(jù)庫索引?
1、 B+樹的磁盤讀寫代價更低:B+樹的內(nèi)部節(jié)點并沒有指向關(guān)鍵字具體信息的指針,因此其內(nèi)部節(jié)點相對B樹更小,如果把所有同一內(nèi)部節(jié)點的關(guān)鍵字存放在同一盤塊中,那么盤塊所能容納的關(guān)鍵字數(shù)量也越多,一次性讀入內(nèi)存的需要查找的關(guān)鍵字也就越多,相對IO讀寫次數(shù)就降低了

2、B+樹的查詢效率更加穩(wěn)定:由于非終結(jié)點并不是最終指向文件內(nèi)容的結(jié)點,而只是葉子結(jié)點中關(guān)鍵字的索引。所以任何關(guān)鍵字的查找必須走一條從根結(jié)點到葉子結(jié)點的路。所有關(guān)鍵字查詢的路徑長度相同,導(dǎo)致每一個數(shù)據(jù)的查詢效率相當。

3、由于B+樹的數(shù)據(jù)都存儲在葉子結(jié)點中,分支結(jié)點均為索引,方便掃庫,只需要掃一遍葉子結(jié)點即可,但是B樹因為其分支結(jié)點同樣存儲著數(shù)據(jù),我們要找到具體的數(shù)據(jù),需要進行一次中序遍歷按序來掃,所以B+樹更加適合在區(qū)間查詢的情況,所以通常B+樹用于數(shù)據(jù)庫索引

PS:我在知乎上看到有人是這樣說的,我感覺說的也挺有道理的:

他們認為數(shù)據(jù)庫索引采用B+樹的主要原因是:B樹在提高了IO性能的同時并沒有解決元素遍歷的我效率低下的問題,正是為了解決這個問題,B+樹應(yīng)用而生。B+樹只需要去遍歷葉子節(jié)點就可以實現(xiàn)整棵樹的遍歷。而且在數(shù)據(jù)庫中基于范圍的查詢是非常頻繁的,而B樹不支持這樣的操作或者說效率太低

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372