前文中,对B树进行了探讨,对于重要知识点进行了说明,但是,关于B树的概念还有些需要具体说明,在了解概念的基础上,实现对B+树的相关操作。
我们描述一个m阶(m>=3)的B树,指的是m一个节点最多有m个孩子节点,当m=2的时候,指的是二叉搜索树。
第一部分:B树的定义
一颗m阶的B树定义如下:
1) 每个节点最多有m-1个关键字(每一个叶子节点都包含k-1个元素,其中 m/2.0 <= k <= m)
2)根节点最少有一个关键字(或者说根节点至少有两个孩子)
3)非根节点至少有Math.ceil(m/2.0) -1个关键字。最多有m-1个关键字 即每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m(这里有一个疑问 :如果m=3 的时候,中间节点的个数至少为1 如果m=4 的时候, 中间节点的个数至少为1 如果m=2的时候,非根节点的节点至少有0个节点么,并不是么 其实我们要强调的是m>=3[解决了第一个问题] )
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于(***)它,而右子树中的关键字都大于(***)它。
5)每个叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
上述一颗阶数为4的B树,但是实际应用中B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针,我们将一个key和其对应的data称为一个记录。在数据库中我们将B(B+)作为索引结构,可以加快查询速度,此时B树中的Key就是键,而data表示了这个键对应的条目在硬盘上的逻辑地址。
示例:以5阶B树为例,说明B树满足的条件
以5阶二叉树为例子,我们可以说明5阶二叉树的特点。
1)每个节点最多有4个关键字。
2)根节点至少有一个关键字。
3)非根节点至少有2个关键字。
4)每个关键字从左到右都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的关键字都大于它。
5)每个叶子节点都位于同一层。
第二部分:B树的搜索
从根节点搜索,找到返回,找不到递归子节点。一直搜索到叶子节点,找到返回,找不到则说明key不存在。
entry BTreeSearch(node, key) {
if(node == null)
return null;
for(int i = 0; i < node.keys.length; i++)
{
if(node.keys[i] == key)
return node.data[i];
}
return BTreeSearch(ChildrenNode[i].node,key);
}
第三部分:B树的插入
B树的插入操作指的是插入一条记录,即<key,value>键值对。如果树中的键已经存在,那么我们需要做的就是更新节点的值。若B树不存在这个key ,那么一定是在叶子节点中进行插入操作(这个和二叉排序树是一致的)。
1.根据要插入的key的值,找到合适的叶子节点位置并进行插入。
2.判断当前的key的个数是否满足关键字的条件,集关键字的最大个数不超过m-1,若满足条件,则结束,否则进行第3步操作。
3.这个时候的叶子节点key的个数等于B树的阶数,这个时候需要进行分裂操作,并将中间节点key拿到父节点中,这个key的左子树指向分裂后的左半部分,这个key的右子枝指向分裂后的右半部分,然后将当前节点指向父节点,继续进行第3步。(我们查看key所在节点的个数是否小于B树的阶数,如果小的话,说明不需要分裂 否则 依次向上层验证,直到根节点。)
示例:下面是一颗5阶B树的插入过程,5阶B数的结点最少2个key,最多4个key。
(1)在空树中插入39
此时根结点就一个key,此时根结点也是叶子结点
(2)继续插入22,97,41
(3) 继续插入53
调整:插入后的节点超过了最大允许的关键字的个数4,所以以key值等于41进行分裂,结果如下图所示。分裂后当前节点指向父节点,满足B树条件,插入操作结束。特别的说明:当阶数m为偶数时,需要分裂时就不存在排序恰好在中间的key,那么我们选择中间位置的前一个key或中间位置的后一个key为中心进行分裂即可。
(4)依次插入13,21,40
调整:
(5)插入30,27, 33
调整后:
插入36,35,34 ;
插入24 29
(5)依次插入26
在实现B树的代码中,为了使代码编写更加容易,我们可以将结点中存储记录的数组长度定义为m而非m-1,这样方便底层的结点由于分裂向上层插入一个记录时,上层有多余的位置存储这个记录。同时,每个结点还可以存储它的父结点的引用,这样就不必编写递归程序。
一般来说,对于确定的m和确定类型的记录,结点大小是固定的,无论它实际存储了多少个记录。但是分配固定结点大小的方法会存在浪费的情况,比如key为28,29所在的结点,还有2个key的位置没有使用,但是已经不可能继续在插入任何值了,因为这个结点的前序key是27,后继key是30,所有整数值都用完了。所以如果记录先按key的大小排好序,再插入到B树中,结点的使用率就会很低,最差情况下使用率仅为50%。
第四部分:B树的删除
删除操作是指,根据key删除记录,如果B树中的记录中不存对应key的记录,则删除失败。
- 如果当前删除的key位于非叶子节点上,则用后继key(后继记录)覆盖要删除的key(就是用直接后继节点的值替换要删除的key),,然后在后继子枝中删除该后继key。此时后继key一定位于叶子节点上[问:这个直接后继节点是叶子节点么???,验证:一般取其中序遍历的直接前驱或者后继作为替换节点,直接后继就是当前节点先往右走,再一直往左走,直到叶子节点的过程 这个叶子节点就是直接后继。],这个过程和二叉搜索树删除节点的方式类似。删除之后要进行第2步
- 该节点key个数大于等于Math.ceil(m/2.0)-1(查看是否满足B树的结构条件:满足节点最少的情况 以5阶为例,则其最少个数为2),如果满足则结束删除操作,否则执行第3步。
- 再查看其兄弟节点key个数大于Math.ceil(m/2.0)-1,则父节点中的key向下移动到该节点,兄弟节点中的一个key向上移,删除操作结束。(向父亲借一个节点,导致父亲少一个不满足原来的B树条件,再向兄弟节点key个数大Math.ceil(m/2.0)-1的借一个,即可满足自身条件,兄弟节点也满足条件)
否则将父结点中的key下移与当前结点及它的兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个孩子指针就变成了一个孩子指针,指向这个新结点。然后当前结点的指针指向父结点,重复上第2步。(这个时候子节点已经不够分了,只有向父节点借一个节点,这样可使当前删除位置的节点满足最少节点条件,但是父节点少了一个 就使得其子树个数减少一个,那么我们需要将当前节点和子树合并,检查合并后,是否满足最少节点的条件,满足则结束 否则继续上述类似操作)
有些结点它可能即有左兄弟,又有右兄弟,那么我们任意选择一个兄弟结点进行操作即可。
示例
(1)原始B树
(2)删除21
删除后结点中的关键字个数仍然大于等于2,所以删除结束。
(3)在上述情况下接着删除27。从上图可知27位于非叶子结点中,所以用27的前驱替换它。从图中可以看出,27的直接前驱为26,我们用26替换27,然后在26(原27)的右孩子结点中删除26。删除后的结果如下图所示。
(4)在上述情况下接着删除32,结果如下图。
当前结点key的个数满足条件,故删除结束。
(5)删除40
合并后结点当前结点满足条件,删除结束。