前言
推出一個新系列,《看圖輕鬆理解資料結構和演算法》,主要使用圖片來描述常見的資料結構和演算法,輕鬆閱讀並理解掌握。本系列包括各種堆、各種佇列、各種列表、各種樹、各種圖、各種排序等等幾十篇的樣子。
B+樹
B+樹是B樹的一種變體,也屬於平衡多路查詢樹,大體結構與B樹相同,包含根節點、內部節點和葉子節點。多用於資料庫和作業系統的檔案系統中,由於B+樹內部節點不儲存資料,所以能在記憶體中存放更多索引,增加快取命中率。另外因為葉子節點相連遍歷操作很方便,而且資料也具有順序性,便於區間查詢。
B+樹特點
- B+樹可以定義一個m值作為預定範圍,即m路(階)B+樹。
- 根節點可能是葉子節點,也可能是包含兩個或兩個以上子節點的節點。
- 內部節點如果擁有k個關鍵字則有k+1個子節點。
- 非葉子節點不儲存資料,只儲存關鍵字用作索引,所有資料都儲存在葉子節點中。
- 非葉子節點有若干子樹指標,如果非葉子節點關鍵字為
k1,k2,...kn
,其中n=m-1,那麼第一個子樹關鍵字判斷條件為小於k1,第二個為大於等於k1而小於k2,以此類推,最後一個為大於等於kn,總共可以劃分出m個區間,即可以有m個分支。(判斷條件其實沒有嚴格的要求,只要能實現對B+樹的資料進行定位劃分即可,有些實現使用了m個關鍵字來劃分割槽間,也是可以的) - 所有葉子節點通過指標鏈相連,且葉子節點本身按關鍵字的大小從小到大順序排列。
- 自然插入而不進行刪除操作時,葉子節點項的個數範圍為[floor(m/2),m-1],內部節點項的個數範圍為[ceil(m/2)-1,m-1]。
- 另外通常B+樹有兩個頭指標,一個指向根節點一個指向關鍵字最小的葉子節點。
- 在進行刪除操作時,涉及到索引節點填充因子和葉子節點填充因子,一般可設葉子節點和索引節點的填充因子都不少於50%。
以下是一棵4階B+樹,
![image](https://i.iter01.com/images/a3789b26c91fa8fc45e8162bce410cecbcb8afb56bd86f8d0e9a37615c14a072.jpg)
插入操作
假設現在構建一棵四階B+樹,開始插入“A”,直接作為根節點,
![image](https://i.iter01.com/images/15294896bf0e92435bcf3958d835062ae9d3b205f42349779ed764ebef938de4.jpg)
插入“B”,大於“A”,放右邊,
![image](https://i.iter01.com/images/48fa47b57f78de564f31dc43355a3c746271f1b9e36aa751038409d773b29a7e.jpg)
插入“C”,按順序排到最後,
![image](https://i.iter01.com/images/b885a3b4ff548d23c1dc368581c852cbe6df39d8042a2557c17af4ab4f1ad4d9.jpg)
繼續插入“D”,直接新增的結果如下圖,此時超過了節點可以存放容量,對於四階B+樹每個節點最多存放3個項,此時需要執行分裂操作,
![image](https://i.iter01.com/images/37975f2f2deb2c173f60cafea5b6d7a904972273daf83f3fdd1e036e16b094f2.jpg)
分裂操作為,先選取待分裂節點中間位置的項,這裡選“C”,然後將“C”項放到父節點中,因為這裡還沒有父節點,那麼直接建立一個新的父節點存放“C”,而原來小於“C”的那些項作為左子樹,原來大於等於“C”的那些項作為右子樹。這裡注意下非葉子節點存放的都是關鍵字,用作索引的,所以父節點存放的“C”項不包括資料,資料仍然存放在右子樹。此外,還需要新增一個指標,由左子樹指向右子樹。
![image](https://i.iter01.com/images/e23df720587120ae547acfb74b1e38d6d075a195138014a1d4e47600672fb49d.jpg)
繼續插入“M”,“M”大於“C”,往右子節點,
![image](https://i.iter01.com/images/ce9bfd2e204f9a00517581c3c3c144f546c10f297232961c22a3488f482d12d8.jpg)
分別與“C”“D”比較,大於它們,放到最右邊,
![image](https://i.iter01.com/images/583598f8a3c7e5c60578c57346e2c5cca47b6162ed917c7e51885ac55338698e.jpg)
插入“L”,“L”大於“B”,往右子樹,
![image](https://i.iter01.com/images/910a29b24b0ef7b01eda6ed78bc7630adac05d4151ac9fee6f74b355a729dca1.jpg)
“L”逐一與節點內項的值比較,根據大小放到指定位置,此時觸發分裂操作,
![image](https://i.iter01.com/images/8ac61f4d839c86850ea889805eb41282509cb3da53cd036f33196530da3a7350.jpg)
選取待分裂節點中間位置的項“L”,然後將“L”項放到父節點中,按大小順序將“L”放到指定位置,而原來小於“L”的那些項作為左子樹,原來大於等於“L”的那些項作為右子樹。父節點存放的“L”項不包括資料,資料仍然存放在右子樹。此外,還需要在左子樹中新增一個指向右子樹的指標。
![image](https://i.iter01.com/images/0d6f533b8da149d51a626ea69d0c1ac662fccaad1ee14a4cfafcd615214c41a7.jpg)
繼續插入“K”,從根節點開始查詢,逐一比較關鍵字,“K”大於“C”而小於“L”,往第二個分支,
![image](https://i.iter01.com/images/34a7c6170ed6ed7c7759c1ea4d1050af8a1e90228857195ee61aaeaf96a67223.jpg)
在子節點中逐一比較,“K”最終落在最右邊,
![image](https://i.iter01.com/images/8fbfd70b7497403e87217d703b059c46a6d4db0390fe6a35f505365768893549.jpg)
繼續插入“J”,從根節點開始查詢,逐一比較關鍵字,“J”大於“C”而小於“L”,往第二個分支,
![image](https://i.iter01.com/images/01109b92dc28de1509e9f7cdbabb9fa26b466e6a1f3954a9bf08eddf58823f6c.jpg)
在子節點中找到“J”的相應位置,此時超過了節點的容量,需要進行分裂操作,
![image](https://i.iter01.com/images/6c228b5917e0331f40170c23240131e5ad4cb68d8a6445b3eec29f60ed9a21b4.jpg)
選取待分裂節點中間位置的項“J”,然後將“J”項放到父節點中,按大小順序將“J”放到指定位置,而原來小於“J”的那些項作為左子樹,原來大於等於“J”的那些項作為右子樹。父節點存放的“J”項不包括資料,資料仍然存放在右子樹。此外,還需要在左子樹中新增一個指向右子樹的指標。
![image](https://i.iter01.com/images/b27f4b6c49082fbd11ee0d16dfc9db212ee72232d66487d1ceb7db6ad9efa4a3.jpg)
繼續插入“I”,從根節點開始查詢,逐一比較關鍵字,“I”大於“C”而小於“J”“L”,往第二個分支,
![image](https://i.iter01.com/images/08c262495ada5150e35b7bbdec1d4c72fc9b0ddcf56119ef96045a4e5d940b46.jpg)
逐一比較找到“I”的插入位置,
![image](https://i.iter01.com/images/8dc4f72b25dc33e13f670126ec7121df07f65b5cb38b85e86e6c1e5a0d72de4e.jpg)
繼續插入“H”,從根節點開始查詢,逐一比較關鍵字,“H”大於“C”而小於“J”“L”,往第二個分支,
![image](https://i.iter01.com/images/68faa9e5eaf1cbe1bb5bf9599bc947940ecadaa322e6f3e09ba9b2cc4891badf.jpg)
“H”逐一與節點內的值比較,根據大小放到指定位置,此時觸發分裂操作,
![image](https://i.iter01.com/images/4ac587e6c65c05071125a806885b0c51d8866a5e086dc7b14e6d5dec19fde937.jpg)
選取待分裂節點中間位置的項“H”,然後將“H”項放到父節點中,按大小順序將“H”放到指定位置,而原來小於“H”的那些項作為左子樹,原來大於等於“H”的那些項作為右子樹。父節點存放的“H”項不包括資料,資料仍然存放在右子樹。此外,還需要在左子樹中新增一個指向右子樹的指標。
但此時父節點超出了容量,父節點需要繼續分裂操作,
![image](https://i.iter01.com/images/775fd7740aa23be00c2e72c58020664fb400cbeb46c4a059d3109611655a762d.jpg)
選取待分裂節點中間位置的項“J”,然後將“J”項放到父節點中,但還不存在父節點,需要建立一個作為父節點。原來小於“J”的那些項作為左子樹,原來大於“J”的那些項作為右子樹。這是非葉子節點的分裂,操作物件都是用作索引的關鍵字,不必考慮資料存放問題。
![image](https://i.iter01.com/images/d2b9dcb195e9ec1057092130264bc1a3f0b05f75edf3abce7118e04ad3e0f1ad.jpg)
插入“G”,從根節點開始查詢,“G”小於“J”,往第一個分支,
![image](https://i.iter01.com/images/c615d88c006173939d650248048eec4436362986640c21fd074c07daffbc0eb8.jpg)
逐一比較節點內項的值,“G”大於“C”小於“H”,往第二個分支,
![image](https://i.iter01.com/images/2f014280d037ae0c5f4bd906c18776e601bca15eb2866e904e82ab2874136ff5.jpg)
逐一比較節點內項的值,找到“G”的位置並插入,
![image](https://i.iter01.com/images/71f651b8103f54cf4c391a1d18b9f707c7267f051aa434fed49eff45b97cedd1.jpg)
插入“F”,從根節點開始查詢,“F”小於“J”,往第一個分支,
![image](https://i.iter01.com/images/0cd105a070e724412dfcea35b4d3c86c110d151d87f4e8192aec244b2fcf0b25.jpg)
逐一比較節點內項的值,“F”大於“C”小於“H”,往第二個分支,
![image](https://i.iter01.com/images/2f0f39de4de88141ed59f1a45e1339ae8eb90d9aa86e792fb2810a702e36b469.jpg)
逐一比較節點內項的值,找到“F”的位置並插入,此時觸發分裂操作,
![image](https://i.iter01.com/images/d2f128c18b4188019ee3f128714ef7ff12250789aceca11ce3ac657713ea5776.jpg)
選取待分裂節點中間位置的項“F”,然後將“F”項放到父節點中,按大小順序將“F”放到指定位置,而原來小於“F”的那些項作為左子樹,原來大於等於“F”的那些項作為右子樹。父節點存放的“F”項不包括資料,資料仍然存放在右子樹。此外,還需要在左子樹中新增一個指向右子樹的指標。
![image](https://i.iter01.com/images/08597861c58de9ab3a1bb50a6b3c6a87fc318f050dac83e26eeac55dc21efbdb.jpg)
最後插入“E”,從根節點開始查詢,“E”小於“J”,往第一個分支,
![image](https://i.iter01.com/images/73495eaed257057cba9052ef0b1f7e2a67f426fb1eeaa2c2bdeb43121f7c374a.jpg)
逐一比較節點內項的值,“E”大於“C”小於“F”,往第二個分支,
![image](https://i.iter01.com/images/a8e25e0e085d957e0501c4262e94373adb40039845cb7e24f56453b0539c6806.jpg)
逐一比較節點內項的值,找打“E”適當的位置並插入。
![image](https://i.iter01.com/images/5fc02ffe2050bc6131dc056e5b4c34ef0e2fc6a4353bf88df3d3d56f8feeb977.jpg)
從上面插入操作可以總結,插入主要就是涉及到分裂操作,而且要注意到非節點只儲存了關鍵字作為索引,而資料都儲存在葉子節點上,此外還需要使用指標將葉子節點連線起來。最終我們可以看到葉子節點的項按從小到大排列,因為有了指標使得可以很方便遍歷資料。
查詢操作
對B+樹的查詢與B樹的查詢差不多,從根節點開始查詢,通過比較項的值找到對應的分支,然後繼續往子樹上查詢。
比如查詢“H”,“H”小於“J”,往第一個分支,
![image](https://i.iter01.com/images/0cb4373049f8470453138068c7397579218ea626ec7dd3bebf3a6bbb399a4f41.jpg)
逐一比較節點中的項,發現應該往第四個分支,
![image](https://i.iter01.com/images/bfdbcaf55e24444c58be31b0fd5c48853460963b2772c94517a49b9b56539c05.jpg)
逐一比較,找到“H”。
![image](https://i.iter01.com/images/ec4e6e053ca63ae55d3f70647d918e71b49f490606c952238d367643b2c3b6d4.jpg)
遍歷操作
遍歷操作首先是要先找到樹最左邊的葉子節點,然後就可以通過指標完成整棵樹的遍歷了。
從根節點開始,一直往第一個分支走,
![image](https://i.iter01.com/images/3bb42852cc7a8abbef4305af668a8b4a6616ba320445ad28bd916dcde93b81bb.jpg)
繼續往第一個分支走,
![image](https://i.iter01.com/images/78679bdbbc3607825def703af48886003393abac7e393b3bdd70d6c7a57d7a86.jpg)
發現已經到葉子節點了,這就是要找的遍歷的開端,
![image](https://i.iter01.com/images/2e8515ad187cfff91fca87a1e04b45cf67f775f92079566eb8e501f93a0b62d1.jpg)
第一個葉子節點有兩個項,接著根據指標跳到第二個葉子節點,
![image](https://i.iter01.com/images/c029e33fc117654f9a55b41ca34a52185a4f9460744e85772cf33e64badd6ade.jpg)
第二個節點有三個項,根據指標繼續往下一個節點,
![image](https://i.iter01.com/images/8fe9c49ad5c9a17412222ebc7c83bf915246ce5ec6ff45b5a83cf8cb67717ec1.jpg)
該節點有兩個項,根據指標繼續往下一個節點,
![image](https://i.iter01.com/images/3d37a483199dcc5762288cffbfa1a7d4b265f75ec3628f7313eb50c3d4deeca6.jpg)
不斷根據指標往下,
![image](https://i.iter01.com/images/956eda27c542f257cab8309fb89b0562ee04983e10c53091973f6010dfe4f8c4.jpg)
往下,
![image](https://i.iter01.com/images/501ef927162e7b098c6d69420f613f7c10b8de31870932b230d767708c3191c1.jpg)
完成整棵樹的遍歷。
![image](https://i.iter01.com/images/ada94f13a23f7fae337ee7c20d9dcc65d85d9c8b90be2a60bad19aea800bb55b.jpg)
-------------推薦閱讀------------
我的開源專案彙總(機器&深度學習、NLP、網路IO、AIML、mysql協議、chatbot)
跟我交流,向我提問:
![看圖輕鬆理解資料結構與演算法系列(B+樹)](https://i.iter01.com/images/4586fe830f825f84da2b07040601b6569decc66a2f80c360ec527425487c4315.png)
歡迎關注:
![看圖輕鬆理解資料結構與演算法系列(B+樹)](https://i.iter01.com/images/5973e6d58ed2a3cf36721ed6c14eb70e3dfc6a82e53d40c00666ca8bfbddff21.jpg)