B树的定义
一颗m阶的B树,或为空树,或为满足以下特性的m叉树:
-
树中每个节点最多含有m棵子树
-
若根节点是非终端节点,则至少右2棵子树。
-
除根节点之外的所有非终端节点至少有⌈m/2⌉棵子树
-
每个非终端节点中包含信息:(n, A0, K1, A1, K2, …,Kn, An)
- K为关键字,升序排序
- A是指向子树根节点的指针,K左边的子树的所有值小于K,K右边的子树的所有值大于K
- 关键字个数满足 ⌈m/2⌉ - 1 <= n <= m - 1
-
叶子节点都在同一层
B树的图示
B树的相关算法
定义结构
我们定义一个节点,这个节点有一个m长度的关键字数组keys,和一个m+1长度的子树指针数组children,还包括一个表明当前节点的关键字数量的属性keyCount,其他属性先暂时不管。keys[i]大于children[i]的所有关键字并且小于children[i+1]所有关键字。
B树的查找
B树的查找算法很简单,从根节点从左往右遍历,直到找到大于或等于要查找的关键字的值Ki或者走到最后,如果等于,就直接返回数据,如果是大于,则说明要找的关键字在Ki-1和Ki之间,则走到Ki-1和Ki之间的子树向下找,如果该节点没有比他大的关键字,则前往最右边的子树。重复该过程,直到遇到叶子节点还没有找到则查找失败。
比如上图找42,走完根节点没有比45大的,于是走最右边的子树,继续往右,发现45比42大,则在45左边的子树向下找,找到42.
B树的插入
B树的插入可以使用查找的算法,一直找到叶子节点,如果找不到,那么就得到了应该插入的位置,如果找到了,就把旧值覆盖。
在插入前,先把插入位置的右边的关键字全部右移,给这个关键字插入提供空间,然后再将要插入的关键字放入应该放入的位置。
可见,插入都在叶节点完成,因此不必移动子树的位置(都是null)。
如果插入了关键字之后,使得关键字的数量等于m,那么这时候不符合二叉树的定义。这个时候我们将这个节点分裂成两个节点。
分裂节点的方法:新生成一个节点,把原来节点的右半部分(不包含中间关键字)复制给这个新节点,左边部分(不包含中间关键字)保留在原来的节点,而中间的关键字则上升至父节点。
如图
这个情况下,子节点需要分裂成两个,并将12向上移动,那么我们需要把要分裂的节点在父节点的索引indexInParent(在这里是1)上的关键字开始到后面全部右移一格,父节点的子树数组上的indexInParent + 1开始到后面的子树也全部右移一格,在这个例子中,只有16右移,和indexInParent右移。得到如下结构
接着,我们把要分裂的节点分裂成2个节点,并且把12向父节点移动,而12左边的子树成为原来节点的最后的子树,12右边的子树成为新节点的第一个子树,假设12的在原来节点的索引是mid, 那么keys[mid + 1…keyCount - 1]和children[mid+1…keyCount]复制到新的节点,而keys[0…mid-1]和keys[0…mid]保留在原来的节点(对子节点的操作在叶子节点没有意义,但是 我们兼顾在非叶子节点的情况,在下面有解释)
通过上面的操作,我们发现,我们给父节点添加了新的关键字,那么此时父节点也有可能出现keyCount == m,则这时候我们需要继续分裂。
如果一直到根节点还需要分裂,就生成一个新的根节点,并把原来的根节点成为新根节点的子节点,然后完成最后一次分裂。
B树的删除
B树的删除分两种情况:
-
删除的数据位于叶子节点
-
删除的数据位于非叶子节点
删除位于非叶子节点时,我们找到该要删除的关键字的右边的子树,从这个子树一直找children[0]直到找到叶子节点,就把叶子节点的key[0]覆盖掉要删除的关键字,然后再删除该叶子节点的key[0](这个关键字实际上是大于要删除的关键字中最小的那个,因此可以直接覆盖掉要删除的key),这样就变成了删除数据位于叶子节点的情况,看下面就好。
删除位于叶子节点时,我们 又分几种情况:
- 删除以后,剩余的关键字数量满足keyCount >= ⌈m/2⌉ - 1, 那么此时只需要直接删除,用右边的关键字覆盖掉左边的关键字即可,keys[index+1…keyCount - 1]全部左移一格。
- 如果删除之后keyCount < ⌈m/2⌉ - 1,那么我们需要找到左边或者右边的兄弟子树,如果兄弟子树满足keyCount > ⌈m/2⌉ - 1,级兄弟节点有冗余,那么我们可以将左兄弟的最大关键字(或者右兄弟的最小关键字)转移到父节点,并将原来父节点的关键字下移至本节点,使keyCount == ⌈m/2⌉ - 1。
- 如果兄弟节点都刚好满足 keyCount == ⌈m/2⌉ - 1,那么我们删除了本节点的这个关键字后,将本节点的剩余关键字,以及本节点和兄弟节点之间包夹的父节点的关键字,一起移动到兄弟节点。合并到一个节点。
第一个情况比较简单,接下来我们看一下后面两个个操作怎么实现
下面的操作也移动了每个节点的孩子节点,同样是因为下列操作有可能作用到非叶子节点
兄弟节点有冗余
如果本节点就是父节点的最左子树,那么我们只能找右兄弟,反之我们只能找左兄弟。
从左兄弟转移
就这个图而言(其实这个时候右节点还满足keyCount == ⌈m/2⌉ - 1,不需要移动,但我们用这个图演示一下怎么移动) ,我们需要将左兄弟的20移动到右边,并将18移动到父节点。并且需要将左兄弟的最右边子树(绿色)移动到左边节点成为最左子树,因为18上去之后,左兄弟会有2个关键字和4个子树,这是不符合规范的,而绿色指针所指向的子树里都是大于18并且小于20的值,20移动到右边并且18移动到上面之后,20左边的子树就是大于18并小于20的子树。我们需要将右边的关键字和子树指针都右移一格,以容纳上面的关键字和左边的指针。移动后如下图
从右节点转移
从右节点转移与从左节点转移相差不大,直接上图
移动前
移动后
兄弟节点无冗余
在兄弟节点有冗余的情况,因为没有更改父节点的关键字个数,我们从兄弟节点调整即可完成删除。
而在兄弟节点无冗余的情况,我们需要将删除后剩余的部分和父节点的一个关键字一起合并到兄弟节点,因此会改变父节点的关键字数量,所以我们需要对父节点再次判断是否满足keyCount >= ⌈m/2⌉ - 1。
同样,我们用图片来展示操作过程
合并到左边
如图,我们要做的就是把20移动到左边,然后把右节点的全部移动到左边,然后父节点剩余关键字左移覆盖掉20,父节点剩余指针左移覆盖掉指向右节点的指针。(上面的蓝色的20是两个子节点中间的关键字,只是刚好是父节点第一个关键字),移动后如下。
合并到右边
向右边移动,我们需要计算左边节点和上面的一个节点一共有多少个,然后右边的兄弟节点关键字和子树指针就向右移动多少,以容纳左边和上边的关键字和子树。然后移动后,父节点关键字和指针也需要向左移动
合并后
这样,我们就完成了向右边合并的操作
操作完成后,如果父节点不满足keyCount == ⌈m/2⌉ - 1我们对父节点做同样的操作,直到父节点满足。
如果父节点为空,说明这个节点是根节点,若此时根节点的关键字个数为0,则需要把根节点的children[0]作为新的根节点。
B树的打印
通过打印可以看到结构是什么样子的
B树的打印的实现可以看我之前写的
https://blog.csdn.net/Baibair/article/details/109406953
下面是效果图
代码实现
下面的代码中,在向左覆盖的时候,并没有把最右边的元素从数组中去掉(太懒了),直接用keyCount控制数组的合法范围。
在删除的方法中写了太多代码没有分开,将就看一看吧
package btree;
public class BTNode<T>{
int keyCount;
int[] keys;
BTNode<T>[] children;
Object[] dataList;
BTNode parent;
int indexInParent = -1;
public BTNode(int m) {
keys = new int[m];
dataList = new Object[m];
children = new BTNode[m + 1];
}
public void add(int index, int key, T data) {
//插入到叶子节点
for (int i = keyCount; i > index; i--) {
dataList[i] = dataList[i - 1];
keys[i] = keys[i - 1];
}
dataList[index] = data;
keys[index] = key;
keyCount++;
}
public void add(int index, int key, T data, BTNode<T> left, BTNode<T> right) {
//插入到非叶子节点,需要控制children
for (int i = keyCount; i > index; i--) {
dataList[i] = dataList[i - 1];
keys[i] = keys[i - 1];
children[i].indexInParent++;
children[i + 1] = children[i];
}
dataList[index] = data;
keys[index] = key;
children[index] = left;
left.parent = this;
right.parent = this;
left.indexInParent = index;
children[index + 1] = right;
right.indexInParent = index + 1;
keyCount++;
}
public int searchIndex(int key) {
int index = 0;
while (index < keyCount && key > keys[index]) index++;
return index;
}
public void transferTo(BTNode<T> node, int index) {