B树包括:B-Tree,B+Tree,B*Tree
多叉树与B树的区别在于:B树的所有叶子结点,都在统一的高度上
B-Tree和B+Tree常在磁盘中做索引
一颗M阶B树T,满足以下条件:
- 每个结点至多拥有M棵子树
- 根结点至少拥有两棵子树( [2, M] )
- 除了根结点以外,其余每个分支结点至少拥有M/2棵子树 ( [M/2, M] )
- 所有的叶结点都在同一层上
- 有k棵子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序( 用于后面的二分查找 )
- 关键字数量满足ceil(M/2)-1 <= n <= M-1 (ceil:向上取整,每个结点存放至少2个至多M-1个关键字)
M阶:表示一个结点最多可以拥有M个子结点
第五点指出非叶子结点的关键字个数 = 指向子结点指针个数 - 1
指针P1指向关键字小于K[1]的子树,指针PM指向关键字大于K[M-1]的子树,P[i]指向关键字属于(K[i-1],K[i])范围的子树
搜索:从根结点开始,对结点内的有序关键字进行二分查找,如果命中则结束,否则进入查询关键字所属范围的子树,重复查找,直到对应的儿子指针为空或已经是叶结点。
由于M/2的限制——
- 在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点
- 在删除结点时,需要将两个不足M/2的兄弟结点合并在一起
插入结点:需要分裂,一定是插在叶子结点上
从根结点开始,找插入key的位置
- 如果根结点满了,需要进行【根节点裂变】,新建一个根结点,然后进行分裂
- 如果插入的结点是非叶结点,如果其孩子结点满了,先进行【子结点裂变】,然后递归下一层,如果不满,直接递归下一层
- 如果插入的结点是叶子结点(因为在判断其父结点时已经保证了它不满),直接插入排序
删除结点:需要合并
从根节点开始查找key,力求找到要删除的key。从根结点开始,我们会找到在当前结点中对应的指针位置idx,它要么代表了要删除的key索引,要么代表了key所在的子结点指针索引(idx)。
如果在当前结点里找不到要删除的key,就一直递归下去,到子结点里查找
如果在当前结点找到了key,又分为四种情况:
- 如果当前结点是叶子结点,直接删除(如果结点的关键字个数为0,需要手动释放结点)
- 如果当前结点不是叶子结点,当前结点的prev结点关键字个数>=degree,用左节点最大值填充key位置,再删除这个最大值
- 如果当前结点不是叶子结点,当前结点的next结点关键字个数>=degree,用左节点最小值填充key位置,再删除这个最小值
- 如果不符合上述情况,就进行合并:把key和右子结点数据加入左子结点中,然后删除当前结点的key,修改关键字和指针
如果搜索到当前结点child = node->chilrends[idx]的个数刚好等于阶数-1(ceil(M/2)-1),需要向前驱子结点left = node->chilrends[idx-1]或后驱子结点right = node->chilrends[idx+1]进行借数,这是出于对BTree性质的保护,如果刚好要删除的key是child,那该结点的个数不满足性质,不能直接删除,所以借数提前保证了结点的num足以被删除一个key。
借数的过程如下:
选择left或right借数,哪个的num大就选哪个,假设right的num更大:把node的数据放到child右边(L下沉到I后面)、把right的的第一个孩子指针放到child最右边(MN放到L下面)、把right的第一个结点放到node当前位置(O上升到原先L的位置)、right所有关键字和孩子指针都往前移一位;
如果left的num是阶数-1,那就需要把left和node->key[idx-1]进行合并;
如果right的num是阶数-1,那就需要把right和node->key[idx]进行合并;
下图所示阶数为3的BTree删除key = B的过程从根节点L开始搜索,idx=0后child结点为F I ,数量刚好等于2(3-1),所以需要借一个数到child最右边,因为这个BTree没有left了,所以只能向right借数,借数流程见上面的绿字,最后再递归到叶子结点,把B删掉。
B+Tree
B+树同样是一个多叉树,包括根结点、内部结点和叶子结点。
B+树和B树的区别:
- 每个叶子节点都有一个指针,指向下一个数据,形成一个有序链表
- 只有叶子结点有data,其他内部结点、根结点都是索引
B树的查找并不稳定,最好的情况是查询根节点,最坏的情况是查询叶子结点
相同数据量的情况下,B+树比B树有以下优势:
- IO次数更少
- 查询性能稳定
- 范围查询更简便