1. 为什么需要B-Tree?
现代计算机的存储器系统是一个具有不同容量、成本和访问时间的存储设备的层次结构。
磁盘访问相比于内存访问是一个非常耗时的操作,若内存访问需要1秒,则一次磁盘访问就相当于1天,所以我们需要尽量减少磁盘访问的次数。
另一方面,在对磁盘进行访问时,通常都是以页或块为单位进行的,我们从磁盘中读一个字节和读一页的时间是差不多的。
多级存储系统中使用B-Tree,可针对外部查找,大大减少I/O次数。
那么平衡二叉树能否的需求?
假设有n = 1G个记录,每次查找需要进行log2(10^9) = 30次I/O操作,每次只读出一个关键码,得不偿失。
而B-Tree充分利用外存对批量访问的高效支持,将此特点转化为优点,每下降一层,都以超级节点为单位,读入一组关键码。
假如上述每个超级节点中有m = 256个关键码,则log256(10^9) = 4,大大减少了IO的次数。
2. B-Tree的结构
B-Tree是平衡的多路搜索树
![c449590aa1a009ed7f63308b1aacfac8.png](https://img-blog.csdnimg.cn/img_convert/c449590aa1a009ed7f63308b1aacfac8.png)
- 每d代合并为超级节点
- m = 2^d路
- m-1个关键码
- 逻辑上与
BBST
完全等价
B-Tree节点:
![26525d21e6af1d23aac9d6217f1b78e7.png](https://img-blog.csdnimg.cn/img_convert/26525d21e6af1d23aac9d6217f1b78e7.png)
B-Tree节点内部主要包括两部分:
- 一组关键码
- 一组指向孩子的指针
其中,关键码的个数总比指向孩子指针的个数少1
![0c14b9091e08865d8bc8bacc1fbb04aa.png](https://img-blog.csdnimg.cn/img_convert/0c14b9091e08865d8bc8bacc1fbb04aa.png)
B-Tree的紧凑表示:
![abc9a57b6b625103ec40335257f3be8b.png](https://img-blog.csdnimg.cn/img_convert/abc9a57b6b625103ec40335257f3be8b.png)
m阶B-Tree的节点分支数需要满足一定的条件:
- 2 <= 根节点分支数 <= m
ceil
(m/2) <= 其他节点分支数 <= m
B-Tree节点的分支数 = 关键码个数+1,所以m阶B-Tree节点的关键码个数需要满足条件:
- 1 <= 根节点关键码个数 <= m-1
ceil
(m/2) - 1 <= 其他节点关键码个数 <= m-1
3. B-Tree的查找
![93c8b8264d05e9c7bf91cd571ff0bb36.png](https://img-blog.csdnimg.cn/img_convert/93c8b8264d05e9c7bf91cd571ff0bb36.png)
B-Tree的查找由一系列的磁盘I/O操作和内存操作组成,每深入一层进行一次磁盘I/O,所以算法的运行时间主要取决于磁盘I/O次数,算法时间复杂度为O(logn)
4. B-Tree的插入
B-Tree的插入三部曲:
- 查找:在当前B-Tree中查找待插入关键码,得到待插入的超级节点
- 插入:在待插入超级节点中插入关键码
- 分裂:如果超级节点关键码个数超过上限,则执行分裂操作
算法:
// from THU-dsacpp
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; //插入成功
}
算法主要步骤:
- 算法首先调用B-Tree的成员函数search查找关键码e
- 这里规定B-Tree中的关键码不重复,所以如果e存在直接返回false
- 如果e不存在,
_hot
表示上一个访问的超级节点,而查找e失败于外部节点,此时_hot
就表示关键码e待插入的叶节点 - 随后在
_hot
节点的关键码中插入e,在指向孩子的指针向量中插入NULL
- 最后调用
solveOverflow
函数,处理上溢(如果有的话)
分裂
主要思想:以上溢节点关键码的中位数s为界,将原节点划分左右两个孩子节点,将s提升一层
![05a00745fac8f10cb26e01a65aa895b9.png](https://img-blog.csdnimg.cn/img_convert/05a00745fac8f10cb26e01a65aa895b9.png)
![df137cfc6c274e9755e2b4071b06ae5f.png](https://img-blog.csdnimg.cn/img_convert/df137cfc6c274e9755e2b4071b06ae5f.png)
5. B-Tree的删除
B-Tree的删除四部曲:
- 查找:首先在B-Tree中查找待删除关键码,得到待删除关键码所处的超级节点v
- 替换:如果v不是叶节点,则找关键码的直接后继(先到右子树,再一路向左),并将关键码与叶节点的第一个关键码替换
- 删除:删除叶节点中的关键码
- 旋转或合并:如果删除后叶节点发生下溢,则执行旋转或合并操作
![93213658013456896db46253c2fa36c9.png](https://img-blog.csdnimg.cn/img_convert/93213658013456896db46253c2fa36c9.png)
旋转
旋转操作的主要思想:发生下溢的节点“左顾右盼”,如果兄弟节点的关键码足够多,则从兄弟节点中“借来”一个关键码。
![c3381049233292de3a33f3d59c4836ff.png](https://img-blog.csdnimg.cn/img_convert/c3381049233292de3a33f3d59c4836ff.png)
合并
![3941e72a3ad6a1056c32ecb025fbd697.png](https://img-blog.csdnimg.cn/img_convert/3941e72a3ad6a1056c32ecb025fbd697.png)
L节点为何恰含有ceil
(m/2) -1个关键码?
如果多于ceil
(m/2)-1,则可以执行旋转操作;如果少于ceil
(m/2)-1,则L自身已经发生下溢。