高级数据结构与算法 | B树、B+树、B*树
文章目錄
- 搜索結(jié)構(gòu)
- B樹
- B樹的插入
- B樹的遍歷
- B樹的性能
- B+樹
- B+樹的插入
- B+樹的遍歷
- B*樹
- B*樹的插入
- 總結(jié)
搜索結(jié)構(gòu)
如果我們有大量的數(shù)據(jù)需要永久存儲,就需要存儲到硬盤之中,但是硬盤的訪問速度遠(yuǎn)遠(yuǎn)小于內(nèi)存,并且由于數(shù)據(jù)量過大,無法一次性加載到內(nèi)存中。此時,就可以考慮將數(shù)據(jù)存儲在硬盤中,而數(shù)據(jù)的地址則加載到內(nèi)存中,通過某種搜索結(jié)構(gòu)進(jìn)行存儲,使用時只需要通過該結(jié)構(gòu)查找到地址,在通過地址去找到對應(yīng)的數(shù)據(jù)即可。
之前我博客中介紹了幾種搜索結(jié)構(gòu):二叉搜索樹、AVL樹、紅黑樹、哈希、位圖、布隆過濾器。
考慮到查找性能以及內(nèi)存消耗,其中適合這種場景的只有平衡二叉搜索樹(AVL、紅黑樹)。
但即使平衡二叉搜索樹的搜索性能能達(dá)到logN,由于數(shù)據(jù)量過于龐大,例如存儲了10億個數(shù),則可能最多需要查找30次。這個數(shù)字看起來不是很多,因為之前我們比較的是內(nèi)存的速度,即使是10億個數(shù)也能一瞬間查找完。但是對于硬盤來說,由于硬盤的速率低,每一次IO都意味這大量的損耗,所以這種方法也不太合適。
如果想要提高查找的效率,那么唯一的方法就是壓縮樹的高度,而壓縮的方法,就是將二叉樹變?yōu)镸叉樹,也就是使用到M路平衡搜索樹,B樹。
B樹
B樹即一棵平衡的M路平衡搜索樹(M > 2),可以是空樹或者滿足以下性質(zhì)
- 根節(jié)點(diǎn)至少有兩個孩子
- 每個非根節(jié)點(diǎn)至少有M/2(上取整)個孩子,至多有M個孩子
- 每個非根節(jié)點(diǎn)至少有M/2-1(上取整)個關(guān)鍵字,至多有M-1個關(guān)鍵字,并且以升序排列
- key[i]和key[i+1]之間的孩子節(jié)點(diǎn)的值介于key[i]、key[i+1]之間
- 所有的葉子節(jié)點(diǎn)都在同一層
節(jié)點(diǎn)設(shè)計
template<class K, int M = 3> struct BTreeNode {K _keys[M]; // 存放元素BTreeNode<K, M>* _pSub[M+1]; // 存放孩子節(jié)點(diǎn),注意:孩子比數(shù)據(jù)多一個BTreeNode<K, M>* _pParent; // 在分裂節(jié)點(diǎn)后可能需要繼續(xù)向上插入,為實現(xiàn)簡單增加parent域size_t _size; // 節(jié)點(diǎn)中有效元素的個數(shù)BTreeNode(): _pParent(NULL), _size(0){for(size_t i = 0; i <= M; ++i)_pSub[i] = NULL;} };B樹的插入
下面拿一個M=3的三叉B樹來舉例子。(ps:三叉樹即每個節(jié)點(diǎn)至多3個孩子,2個key,key的數(shù)量永遠(yuǎn)比孩子少一個)
假設(shè)使用以下數(shù)據(jù)構(gòu)建B樹{63, 131, 85, 39, 148, 31, 111}(B樹從葉子節(jié)點(diǎn)的位置進(jìn)行插入)
首先依次插入前三個節(jié)點(diǎn),當(dāng)插入到第三個時,由于三叉樹的key只能有2個,所以此時會采取分裂的方法來維持樹的平衡。
B樹分裂的規(guī)則是:創(chuàng)建一個兄弟節(jié)點(diǎn),拷貝右半?yún)^(qū)間的數(shù)據(jù)到兄弟節(jié)點(diǎn),左半?yún)^(qū)間保留,中位數(shù)放到父親節(jié)點(diǎn)(如果沒有則創(chuàng)建新的根節(jié)點(diǎn))。
B樹與AVL的旋轉(zhuǎn),紅黑樹的旋轉(zhuǎn)+變色不一樣,它使用了分裂的方法來維持樹的平衡,這樣的好處是既能做到平衡,也保證了非根節(jié)點(diǎn)至少有一半的空間利用率
所以此時按照上面的規(guī)則進(jìn)行分裂
接著插入39, 148。
此時插入31,開始分裂
接著插入111,此時再次發(fā)生分裂
此時可以看到,葉子節(jié)點(diǎn)已經(jīng)分裂成了四個,并且根節(jié)點(diǎn)的key也達(dá)到了三個,所以此時規(guī)則不成立,按照分裂的規(guī)則繼續(xù)分裂
此時,樹重新平衡。所以從上面可以看出來,本質(zhì)上B樹的設(shè)計還是參考了二叉搜索樹。
B樹的遍歷
B樹的有序遍歷還是通過中序遍歷來完成,不過需要通過隊列或者遞歸遍歷完這個節(jié)點(diǎn)的所有的key
如
B樹的性能
作為一個M路平衡搜索樹,B樹的搜索性能達(dá)到了 logM+1Nlog_{M+1}{N}logM+1?N~logM/2Nlog_{M/2}{N}logM/2?N之間,查找到對應(yīng)的節(jié)點(diǎn)后,通過節(jié)點(diǎn)存儲的地址來進(jìn)行二分查找,就可以確定數(shù)據(jù)的位置。比起二叉平衡搜索樹,速度快了一大截,并且大大的減少了硬盤IO的次數(shù),所以在文件系統(tǒng)以及數(shù)據(jù)庫索引等方面使用的都是這種數(shù)據(jù)結(jié)構(gòu)。
B+樹
B+樹是B樹的變形,主要性質(zhì)如下
- 其定義基本與B樹相同
- 非葉子節(jié)點(diǎn)的孩子與key個數(shù)相同(簡化規(guī)則)
- 非葉子節(jié)點(diǎn)由葉子節(jié)點(diǎn)的最小值構(gòu)成(充當(dāng)索引,所以不可能在非葉子節(jié)點(diǎn)命中)
- 所有數(shù)據(jù)都出現(xiàn)在葉子節(jié)點(diǎn),并且所有葉子節(jié)點(diǎn)都鏈接起來,同時是有序的。(方便遍歷)
如下圖
B+樹的插入
這里為了方便,使用三階B+樹舉例子,這里還是使用同樣的數(shù)據(jù)。{63, 131, 85, 39, 148, 31, 111}
首先插入63,并把63作為父節(jié)點(diǎn)的索引
接著插入131, 85
當(dāng)插入39時,發(fā)生分裂,分裂規(guī)則與之前略有區(qū)別。
B+樹的分裂規(guī)則發(fā)生了變化,因為此時父節(jié)點(diǎn)存儲的是索引,所以此時只會將左半部分?jǐn)?shù)據(jù)保留,右半部分?jǐn)?shù)據(jù)放入新建的兄弟節(jié)點(diǎn),并且會向上更新父節(jié)點(diǎn)的索引。
接著插入148, 31
當(dāng)插入111時,發(fā)生分裂
B+樹的遍歷
從上面可以看出來,B+樹的主要特點(diǎn)其實就是更方便進(jìn)行遍歷,因為其將所有數(shù)據(jù)存儲在葉子節(jié)點(diǎn),并且將所有葉子節(jié)點(diǎn)連接起來,就可以像遍歷鏈表一樣遍歷B+樹。而所有非葉子節(jié)點(diǎn)就相當(dāng)于一個索引,這樣的結(jié)構(gòu)使得B+樹的查找相當(dāng)于對關(guān)鍵字全集做一次二分查找。
所以通常文件的索引系統(tǒng)都會采用B+樹的結(jié)構(gòu)。
B*樹
B*樹則又是對B+樹的變形,其性質(zhì)如下
- 其定義基本與B+樹相同
- 將非葉子節(jié)點(diǎn)也連接起來
- 分裂方式再次修改,保證每個節(jié)點(diǎn)中key的數(shù)量[2/3 * M, M](提高空間利用率,從1/2提升到了2/3)
B*樹的插入
B*樹再次修改了插入規(guī)則,規(guī)則修改為如果當(dāng)前節(jié)點(diǎn)數(shù)據(jù)已滿而兄弟節(jié)點(diǎn)未滿,則將數(shù)據(jù)放入兄弟節(jié)點(diǎn),而當(dāng)兩個節(jié)點(diǎn)都滿了之后再進(jìn)行分裂,在原節(jié)點(diǎn)與兄弟節(jié)點(diǎn)之間創(chuàng)建新節(jié)點(diǎn),從兩個節(jié)點(diǎn)分別取出1/3的數(shù)據(jù)放入新創(chuàng)建的結(jié)點(diǎn)。
還是原來那些數(shù)據(jù){63, 131, 85, 39, 148, 31, 111}
此時插入111,發(fā)生分裂,從兩邊各取走1/3的數(shù)據(jù)
從上面可以看出,B*樹的最大改進(jìn)就是將B+樹的空間利用率從1/2提升到了2/3,并且對非葉子節(jié)點(diǎn)也進(jìn)行了連接,查找更加便利。
總結(jié)
總結(jié)
以上是生活随笔為你收集整理的高级数据结构与算法 | B树、B+树、B*树的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【项目介绍】协程——C语言实现的用户态非
- 下一篇: 高级数据结构与算法 | 跳跃表(Skip