在学习B树和B+树之前首先考虑一个问题:为什么需要B树、B+树?普通的二叉树或者平衡二叉树不能满足我们的需求吗?
我们知道,B树和B+树一个典型的应用场景是作为数据库索引,下面我们就以这个场景为例,分析一下B树和普通二叉树的区别。
假设我们的数据库存放在磁盘上,有n = 1G
个记录。
如果我们用平衡二叉树建立索引,那么每次查找需要
![equation?tex=log_210%5E9](https://i-blog.csdnimg.cn/blog_migrate/d31f366263703e571f0bdeb0fbf474ac.png)
两点事实:
- 不同容量的存储器,访问速度差异悬殊
- 从磁盘中读写
1B
,与读写1KB
几乎一样快
为了充分利用磁盘对批量访问的高效支持,将平衡二叉树中每d代合并为一个超级节点,每下降一层,都以超级节点为单位,读入一组关键码。所谓m阶B树,即m路平衡搜索树。
如果我们用m=256阶B树建立索引,那么每次查找需要
![equation?tex=log_m10%5E9](https://i-blog.csdnimg.cn/blog_migrate/930db25993d5d938b5943c03fa18e57b.png)
1. 结构
B树相当于把平衡二叉树中每d代合并为一个超级节点
平衡二叉树:
![e9109f8be83a70b3fc8e607df3c385b1.png](https://i-blog.csdnimg.cn/blog_migrate/4fe26ca3c5e1e91e8808031a651c0a72.png)
B树:
![70177ffefb9e3a5f9316727684033423.png](https://i-blog.csdnimg.cn/blog_migrate/5b8f280f99f260bb4a32ce108df4ced6.png)
从上图我们可以看到,每个B树超级节点中包含3个关键码和4个指向下层节点的指针。
一般地,一个超级节点包含一组关键码和一组指向孩子节点的指针,关键码个数总比孩子指针个数少一个。
![d67ac034ccefc71bfcab65ab34ff03a9.png](https://i-blog.csdnimg.cn/blog_migrate/7ffc645fa95f609592eec8eb6c8796ae.png)
m阶B树的约束条件:
- 树根:2 <= 分支数 <= m
- 其余节点:
ceil(m/2)
<= 分支数 <= m
B树的紧凑表示:
![74a78a7aa5d7ea799f7bb4efeb30ce70.png](https://i-blog.csdnimg.cn/blog_migrate/ea74c7f13ab28049eb24431c78088c0e.png)
B树索引:
B树索引的叶节点和非叶节点如下图所示,(a)表示B树索引的叶节点,其中指针Pi指向具有搜索码值Ki的一条文件记录。(b)表示B树索引的非叶节点,其中指针Pi指向孩子节点,Bi指向搜索码Ki对应的一条文件记录。
![3b1b17748574269317ac338af724ca8a.png](https://i-blog.csdnimg.cn/blog_migrate/81613a0d3cbfd339f54f85f88bfa3254.png)
B树索引的一个示例:
![c1761a8a25e91694de74ac2eb38d0af7.png](https://i-blog.csdnimg.cn/blog_migrate/a6a908e162e2b955a4b59449171fb609.jpeg)
2. 查找
![996ebfcaf13a6748893f7b17e9ac364d.png](https://i-blog.csdnimg.cn/blog_migrate/652c8488201373c92ce1b0a6cbccf3df.jpeg)
算法:
- 将根节点作为当前节点
- 只要当前节点非外部节点
- 在当前节点中顺序查找
- 若找到关键码,则
- 返回查找成功
- 否则
- 沿指针,转至对应子树
- 将其根节点读入内存
- 更新当前节点
- 返回查找失败
查找示例:
![e48ad025b046ba7fa8ddfc5dad032086.gif](https://i-blog.csdnimg.cn/blog_migrate/87a3a10fd4c004e097dbc5b3d1dd82e2.gif)
3. 插入
算法:
bool BTree<T>::insert( const T & e ) {
BTNodePosi(T) v = search( e );
if ( v ) return false; //确认e不存在
Rank r = _hot->key.search( e ); //在节点_hot中确定插入位置
_hot->key.insert( r+1, e ); //将新关键码插至对应的位置
_hot->child.insert( r+2, NULL ); _size++; //创建一个空子树指针
solveOverflow( _hot ); //若上溢,则分裂
return true; //插入成功
}
其中_hot
节点表示上一个查找所访问的超级节点。
算法首先在B树中查找关键码e,确保e不存在(假设B树中各个关键码唯一)。查找必然失败于某个节点_hot
,然后在该叶节点的适当位置插入关键码和一个空子树指针。
由于B树节点对关键码的最大数目有限制,所以在叶节点插入一个新的关键码可能导致节点中关键码个数上溢,此时需要调用solveOverflow
函数,将上溢节点分裂。
分裂:
设上溢节点中关键码依次为{k0, k1, k2, ..., km-1}
- 取中位数s = m/2, 以关键码ks为界将关键码划分为:{k0, k1, ..., ks-1}, {ks}, {ks+1, ..., km-1}
- 关键码ks上升一层,并分裂,以所得的两个节点为左右孩子
![8a67af66f07b10839506646aa8a0350a.png](https://i-blog.csdnimg.cn/blog_migrate/bb3629c6b5de4f0fdeb56627f0a883bd.jpeg)
插入示例:
1.无需分裂
![f150db1695fd58638a9e135a98562de0.png](https://i-blog.csdnimg.cn/blog_migrate/9047ead1d18878610d178ae2b98ad308.png)
2.分裂一次
![29f54180cd02b13129419dfe7bc1a966.gif](https://i-blog.csdnimg.cn/blog_migrate/02ee5718ef6b286e437605e10569410f.gif)
3.分裂两次
![3bd9b6f4757a8b42867cf16bffefe16e.png](https://i-blog.csdnimg.cn/blog_migrate/81472cfd8561079d19a0efc89a063d62.png)
4.分裂到根
![c9f45bab0443f2e562845238ecddf256.png](https://i-blog.csdnimg.cn/blog_migrate/c49ec78eecafba4a88ac54cc44d67ae0.png)
4. 删除
算法:
bool BTree<T>::remove( const T & e ) {
BTNodePosi(T) v = search( e );
if ( ! v ) return false; //确认e存在
Rank r = v->key.search(e); //e在v中的秩
if ( v->child[0] ) { //若v非叶子,则
BTNodePosi(T) u = v->child[r + 1]; //在右子树中一直向左,即可
while ( u->child[0] ) u = u->child[0]; //找到e的后继(必属于某叶节点)
v->key[r] = u->key[0]; v = u; r = 0; //并与之交换位置
} //至此,v必然位于最底层,且其中第r个关键码就是待删除者
v->key.remove( r ); v->child.remove( r + 1 ); _size--;
solveUnderflow( v ); return true; //如有必要,需做旋转或合并
}
删除算法首先在B树中查找关键码e,确认e存在。如果e位于非叶节点,则需要将它与它的后继交换位置,这样待删除节点必然位于页节点。然后将页节点中的关键码e删除,并删除一个空指针。
由于B树对关键码的最少数目有要求,所以删除可能导致叶节点中的关键码个数发生下溢,此时需要进行旋转或合并操作来进行修复。
旋转:
旋转的思想是在发生下溢的节点“左顾右盼”,如果它的左兄弟或者右兄弟中有足够多(至少ceil(m/2)
)个的关键码,则从它的兄弟中“借出”一个关键码。
![d7b0b976ffb04ebcc194bbfc5409957f.png](https://i-blog.csdnimg.cn/blog_migrate/6271d77c1b37dbe85a8736d15198769d.jpeg)
算法:
- 若L存在,且至少包括
ceil(m/2)
个关键码 - 将P中的分界关键码y移入V中(作为最小关键码)
- 将L中最大的关键码x移入P中(取代原关键码y)
- 若R存在,且至少包含
ceil(m/2)
个关键码 - 也可旋转,完全对称
合并:
![973877810a340f690a1004c1323c456c.png](https://i-blog.csdnimg.cn/blog_migrate/fdcac66f410d657a4b8f69eb00ca7feb.jpeg)
算法:
- L和R或不存在,或均不足
ceil(m/2)
个关键码——即便如此,L和仍必有其一,且恰含ceil(m/2)
-1个关键码 - 从P中抽取介于L和V之间的分界关键码y
- 通过y做粘接,将L和V合成一个节点
- 同时合并此前y的孩子引用
- 此处下溢得到修复,但可能导致P下溢(此时继续修复操作)
删除示例:
1.直接删除
![36ecf530a686ef481249bfdd7b22062f.gif](https://i-blog.csdnimg.cn/blog_migrate/f1148780283cb7bee178017cc20a174d.gif)
2.旋转
![5fc3e58c7b362c59ab39aeba5a92bb92.png](https://i-blog.csdnimg.cn/blog_migrate/80d95df1ca7f42eca08cc695b18dd54c.png)
3.单次合并
![5418533933b52efe523a027435139e6e.png](https://i-blog.csdnimg.cn/blog_migrate/5f9ebe0ed0d1e10dac72100c1a88d9fd.png)
4.多次合并
![3e89068629d24382817991bb203ba81a.png](https://i-blog.csdnimg.cn/blog_migrate/8ebf56f4addaca7d194c0921ba35d5b7.png)