B+樹重要操作
中間節點
- 中間節點的key,與其對應的指標的原則是,小於key的元素在其指標指向的節點中
- 中間節點的key可以看成是右斜著排放的,即小於等於key的節點由key對應的指標指定,最有一個指標指向大於最右側key的節點
分裂
- 當中間節點數量滿了時,進行分裂,新生成一個相鄰的中間節點right_internal_node
- 計算原先保留多少元素, 例如保留left_count = K/2
- 將left_count對應的key作為upper_key後續插入到中間節點
- 從left_count + 1下標開始,將key複製到新生成的節點中
- 將left_count下表對應的指標留在左節點
- 將left_count +1 下表對應的指標開始複製到新生成的節點中
分裂後插入上層中間節點
- 經過分裂有三個重要元素:left_internal_node(也就是原先被分裂節點), right_internal_node(新生成的節點),upper_key
- 上層中間節點為upper_internal_node
- 注意原先upper_internal_node有一個old_key和old_ptr指標指向left_internal_node,這個old_key肯定是比upper_key大的
- 將(upper_key, left_internal_node)作為一對插入到upper_internal_node
- 將old_key對應的old_ptr改成指向right_internal_node
刪除
借入資料
中間節點刪除資料後,如果數量少於最低值K/2,那麼它可以嘗試從它的相鄰節點借入資料。那麼它的相鄰節點在哪裡呢?第一類相鄰節點是與中間節點共享一個父節點的節點,很容易透過其父節點找到。第二類相鄰節點,不與其共享一個父節點:想象一下當節點是父節點的最左邊節點時,那麼在其父節點下,它只有一個右側相鄰節點,這個時候它的左側相鄰節點不在其父節點下;當節點是父節點的最右邊節點時,那麼在其父節點下,它只有一個左側相鄰節點,這時候它的右側相鄰節點不在其父節點下,而在右側樹分支下。
- 注意第二類的借入在實現的時候也可以不考慮,只不過這樣會過早的帶來節點合併。
節點合併
當節點確實從左右都沒有節點可以借入資料時,那麼它就可以與左右節點合併了。正因為其左右節點也沒有多的節點可以借,說明其左右節點不大於K/2,那麼合併後節點節點數量是會小於K的。
節點合併,我們可以總是選擇與其相同父節點下的相鄰節點進行合併。節點合併後,需要使父節點刪除掉被合併資料的節點(需要注意的是在實際操作中,我們可以選擇被刪除的節點最終保留下來,也可以選擇相鄰節點最終保留下來,這只是實現方式)。
降級
父節點刪除某個子節點後,需要檢查父節點的key數量是否滿足K/2要求,如果父節點不滿足,那麼父節點也需要執行借入動作,如果父節點無法接入,那麼就需要執行合併動作。並最終出發祖父節點的刪除動作。整個流程會遞迴上去最終可能會到根節點,若根節點刪除了資料,且一個key也沒有了。那麼就要將其子節點設定成新的root了(奇怪?沒有key還有子節點?注意中間節點指標比key多一個,就是說沒有key,也至少有一個指標)。
葉子節點
插入
分裂
- 當葉子節點數量滿了時,進行分裂,新生成一個相鄰sibling節點
- 計算原先保留多少元素, 例如保留left_count = K/2
- 將下標left_count開始的key移動到新生成的節點
- 將left_count對應的key作為upper_key後續插入到中間節點
- 將下標left_count開始的value移動到新生成的節點
分裂後插入上層中間節點
- 新分裂出的節點的第一個key為upper_key
- 如果葉子節點沒有上層中間節點,那麼就新建一箇中間節點作為根節點,把原先葉子節點以及新分裂出的葉子節點直接插入到新的中間節點。同時將upper_key插入到新中間節點中。即它有1個key,兩個指標。
- 如果有上層中間節點
- 首先將upper_key插入到中間節點
- 其次將新分裂出的節點指標也插入到中間節點,這裡需要注意的是由於中間節點的指標數量比key數量多一個。所以指標插入的index要比upper_key插入的index大1
刪除
- 刪除後,嘗試從其右側的節點借資料,前提是右側節點包含m/2 +1個元素
- 若節點(且該節點不是中間節點的最左側節點)最左邊一個元素被刪除了,則需要更新中間對應的key
借入資料
借入資料的流程是簡單的,與中間節點類似。找到其相鄰節點並拿到資料。同樣需要注意的是它的相鄰節點也可能不在其父節點下。
節點合併
與中間節點的操作類似,找到相鄰節點,複製資料。通知父節點刪除一個節點即可。
要記錄哪些指標
- 從B+樹的定義上來看,至少每個葉子節點需要記錄一個指向右側相鄰節點的指標
其他指標要不要記錄呢?
- 節點是否要記錄父節點指標?
- 中間節點是否要記錄右側節點的指標?
- 節點是否要記錄其左側節點的指標?
這個因設計實現而異了,記錄更多的指標,那麼在尋找定位的時候肯定更加容易,但是也帶來了很多維護成本,這個在設計的時候做考量。
如何找到相鄰的節點
相同父節點下的相鄰節點
很容易,很自然,進行遍歷就可以或者對key進行find或二分法(注意所有節點裡面的key都是有序的)
非相同父節點下的相鄰節點
- 最節點的方式是記錄節點(包括中間節點)的左側、右側指標
另外一種方式,如果不想過多的記錄指標,那麼可以透過搜尋回溯的方式來找目標節點到相鄰節點。 某個最左側中間節點為例:
- 從它開始往上回溯,當回溯到的節點不是其父節點的最左側節點時,說明我們已經找到了共同祖先
- 然後從共同祖先中的左側節點(對應下圖1的左側節點3)開始往下找其最右側的子節點,當往下搜尋的層數與向上回溯的層數相同時,我們就找到了目標節點的相鄰節點。
- 最右側中間節點的搜尋過程是類似的。
例如想找到節點5的做相鄰節點4,就往上回溯首先到1, 5是1的最左側節點,則繼續向上到2,1不是2的最左側節點,所有2就是共同祖先了。1的左側節點3往下,找到最右側節點4, 4與5是相同層級的,那麼就是5的相鄰左節點了。
另外一個問題是如何進行回溯呢?一種實現方式是子節點記錄父節點的指標;另一種實現方式是,記錄搜尋過程的搜尋棧(即從根節點到葉子節點經過了哪些中間節點,記錄到一個列表裡面)。