把B树压缩成数组的结构, 二维的树是由 一维的数组 进化(拉伸)而来的,所以我们先还原成 一维数组 可以简化对删除操作的思考(如前趋值、后趋值的概念就是一维数组中的)。
实质上,B树的删除和二叉树的删除很相似,都是用其左/右分支的最大值/最小值来覆盖(删除)当前要删除的关键字,然后再递归删除此左/右分支的此关键字。
而无论是二叉树还是B树,也无论是删除还是插入,最后的任务都落在叶子结点上!
首先,每次删除开始之前,我们考查根结点,看是否符合合并的规则,如果合并,此时树的高度减1,这是树高度减少的唯一途径,此外合并也是为了应付要删除的关键字也可能就是根结点的情况。
接着,从根(如果合并就从新的根开始)开始删除,查找当前结点。
在删除的过程中,可能会出现两种情况,
1)要删除的关键字在当前结点中,如果其左/右分支的根结点(下一次递归新的根结点)的关键字个数至少为 t 个(也是为了★),就用其左/右分支的最大值/最小值来覆盖(删除)当前要删除的关键字,然后再递归删除此左/右分支的此关键字(以此左/右分支的根结点为新的根,★);如果都为 t -1 个,就不能保证在下次递归调用时包括关键字的子树根(新的根)的关键字数至少为t(除了根结点外),所以应该根结点下拉合并再删除。这么做的目的是不破坏B树的结构,就像二叉树的删除一样。
2)要删除的关键字不在当前结点中,但是我们可以确定关键字所在的(当前结点的)分支。看此分支根结点关键字个数是否等于 t – 1 个,如果是,就向兄弟结点经父节点借关键字(向左借/向右借/下拉合并)——(借过之后,所借的兄弟结点的关键字个数减1,所以要保证所借结点的关键字个数至少是 t ,否则就要把根结点关键字下拉合并),借和合并的目的也是为了保证在下一次递归时新的根关键字个数至少为 t 。然后以此左/右分支的根结点为新的根(也是至少为 t )递归向下查找。
当然如果至少为 t 个,不借也不合并了,递归向下删除即可。
这么做,都是为了保证此分支的根结点的关键字个数至少为 t ,从而为将来可能发生的下拉做好准备。
★:从上面可以看出,我们必须时刻保证包含要删除关键字的根结点的关键字数量至少为t,这样做就是为了确保合并的成功(所谓合并,就是根结点的一个关键字下拉到此关键字的左子结点中且把右子结点合并到左子结点中——当然,前提是此时左结点无法从其相邻兄弟结点借关键字,即相邻兄弟结点的关键字个数皆为 t – 1 个)。使得在每次递归调用前,程序都能保证包括关键字的子树根的关键字数至少为t(除了根结点外)。
具体做法如下:
在删除B树节点时,为了避免回溯,当遇到需要合并的节点时就立即执行合并,B树的删除算法如下:从root向叶子节点按照search规律遍历:
(1) 如果target在叶节点x中,则直接从x中删除target,情况(2)和(3)会保证当再叶子节点找到target时,肯定能借节点或合并成功而不会引起父节点的关键字个数少于t-1。
(2) 如果target在分支节点x中:
(a) 如果x的左分支节点y至少包含t个关键字,则找出y的最右的关键字prev,并替换target,并在y中递归删除prev。
(b) 如果x的右分支节点z至少包含t个关键字,则找出z的最左的关键字next,并替换target,并在z中递归删除next。
(c) 否则,如果y和z都只有t-1个关键字,则将targe与z合并到y中,使得y有2t-1个关键字,再从y中递归删除target。
(3) 如果关键字不在分支节点x中,则必然在x的某个分支节点p[i]中,如果p[i]节点只有t-1个关键字。
(a) 如果p[i-1]拥有至少t个关键字,则将x的某个关键字降至p[i]中,将p[i-1]的最大节点上升至x中。
(b) 如果p[i+1]拥有至少t个关键字,则将x个某个关键字降至p[i]中,将p[i+1]的最小关键字上升至x个。 //利用父结点周转的情况
(c) 如果p[i-1]与p[i+1]都拥有t-1个关键字,则将p[i]与其中一个兄弟合并,将x的一个关键字降至合并的节点中,成为中间关键字。
代码如下:
// 将y、root->k[pos]、z合并到y节点,并释放z节点(前提是 y,z 各有M-1个节点)
void btree_merge_child(btree_node *root, int pos, btree_node *y, btree_node *z)
{
// 将z中节点拷贝到y的后半部分
y->num = 2 * M - 1;
for(int i = M; i < 2 * M - 1; i++) {
y->k[i] = z->k[i-M];
}
y->k[M-1] = root->k[pos]; // k[pos]下降为y的 中间 节点
// 如果z非叶子,需要拷贝pointer
if(false == z->is_leaf) {
for(int i = M; i < 2 * M; i++) { //k[pos]下降为y的中间节点后,y指针的个数并没有增加
y->p[i] = z->p[i-M];
}
}
// 根的k[pos]下降到y中,更新根的key和pointer
for(int j = pos + 1; j < root->num; j++) {
root->k[j-1] = root->k[j];
root->p[j] = root->p[j+1]; //完全正确,因为在挪动root->k后,使得现在pos+1的指向就应该是原来pos+2的指向
}
root->num -= 1; //根的关键字个数减 1
free(z);
}
// 删除入口,即每次删除一个数都要先检查根及其两个子女是否需要合并(高度减1的唯一方法)—注意合并后的根
btree_node *btree_delete(btree_node *root, int target)
{
// 特殊处理,当根只有两个子女,且两个子女的关键字个数都为M-1时,合并根与两个子女
// 这是唯一能降低树高的情形
if(1 == root->num) {
btree_node *y = root->p[0];
btree_node *z = root->p[1];
if(NULL != y && NULL != z &&
M - 1 == y->num && M - 1 == z->num) {
btree_merge_child(root, 0, y, z);
free(root);
btree_delete_nonone(y, target); //合并后的根是 y
return y;
} else {
btree_delete_nonone(root, target);
return root;
}
}
else {
btree_delete_nonone(root, target);
return root;
}
}
/* root至少有个t个关键字,保证不会回溯。
在当前结点中,就覆盖并向下递归删除;
不在当前结点中,就要么就把子结点通过当前结点(父节点)周转 或者 合并,然后再向下递归删除*/
void btree_delete_nonone(btree_node *root, int target)
{
if(true == root->is_leaf) { // 如果在叶子节点,直接删除(在之前可能已经合并过,所以不用担心删除后的关键字个数会小于t)
int i = 0;
while(i < root->num && target > root->k[i]) i++;
if(target == root->k[i]) {
for(int j = i + 1; j < 2 * M - 1; j++) {
root->k[j-1] = root->k[j];
}
root->num -= 1;
} else {
printf("target not found\n");
}
}
else { // 在分支中
int i = 0;
btree_node *y = NULL, *z = NULL;
while(i < root->num && target > root->k[i])
i++;
if(i < root->num && target == root->k[i]) { // 如果在分支节点找到target
y = root->p[i]; //左分支
z = root->p[i+1];//右分支
if(y->num > M - 1) {
// 如果左分支关键字多于M-1,则以y为根找到左分支的最右节点prev,替换target
// 并在左分支中递归删除prev,情况2(a)
int pre = btree_search_predecessor(y);
root->k[i] = pre; //覆盖即删除
btree_delete_nonone(y, pre);
}
else if(z->num > M - 1) {
// 如果右分支关键字多于M-1,则找到右分支的最左节点next,替换target
// 并在右分支中递归删除next,情况2(b)
int next = btree_search_successor(z);
root->k[i] = next;
btree_delete_nonone(z, next);
}
else {
// 两个分支节点数都为M-1,则合并至y,并在y中递归删除target,情况2(c)
btree_merge_child(root, i, y, z);
btree_delete_nonone(y, target); //换成 btree_delete(y, target);也是正确的
}
}
else {
// 在分支没有找到,肯定在分支的子节点中,注意,此时,只向下一层!!!!一层!!!不是向上面那样一直找到叶子结点
y = root->p[i]; //i已经前进过了。y是一定包含关键字的根
if(i < root->num) { //指针的数量比关键字多1
z = root->p[i+1];
}
btree_node *p = NULL;
if(i > 0) {
p = root->p[i-1];
}
if(y->num == M - 1) {
if(i > 0 && p->num > M - 1) {
// 左邻接节点关键字个数大于M-1
btree_shift_to_right_child(root, i-1, p, y); //情况3(a),注意位置变成了i-1
}
else if(i < root->num && z->num > M - 1) {
// 右邻接节点关键字个数大于M-1
btree_shift_to_left_child(root, i, y, z); // 情况3(b)
}
else if(i > 0) {
btree_merge_child(root, i-1, p, y); // 情况3(c) ,y和前面的合并
y = p;
}
else {
btree_merge_child(root, i, y, z); // 情况3(c),如果i=0,且y后面的也是M-1个,则y和后面的合并
}
btree_delete_nonone(y, target);
}
else {
btree_delete_nonone(y, target); //如果 y->num >= M
}
}
}
}
//寻找rightmost,以root为根的最大关键字
int btree_search_predecessor(btree_node *root)
{
btree_node *y = root;
while(false == y->is_leaf) { //根据B树的形状和规则,(某根)最右或最左的关键字一定在叶子结点中
y = y->p[y->num];
}
return y->k[y->num-1];
}
// 寻找leftmost,以root为根的最小关键字
int btree_search_successor(btree_node *root)
{
btree_node *z = root;
while(false == z->is_leaf) {
z = z->p[0];
}
return z->k[0];
}
// z向y借节点,将root->k[pos]下降至z,将y的最大关键字上升至root的pos处
void btree_shift_to_right_child(btree_node *root, int pos,
btree_node *y, btree_node *z)
{
z->num += 1;
for(int i = z->num -1; i > 0; i--) {
z->k[i] = z->k[i-1];
}
z->k[0]= root->k[pos];
root->k[pos] = y->k[y->num-1];
if(false == z->is_leaf) {
for(int i = z->num; i > 0; i--) {
z->p[i] = z->p[i-1];
}
z->p[0] = y->p[y->num];
}
y->num -= 1;
}
// y向借z节点,将root->k[pos]下降至y,将z的最小关键字上升至root的pos处
void btree_shift_to_left_child(btree_node *root, int pos,
btree_node *y, btree_node *z)
{
y->num += 1;
y->k[y->num-1] = root->k[pos];
root->k[pos] = z->k[0];
for(int j = 1; j < z->num; j++) {
z->k[j-1] = z->k[j];
}
if(false == z->is_leaf) {
y->p[y->num] = z->p[0]; //这句是必须的
for(int j = 1; j <= z->num; j++) {
z->p[j-1] = z->p[j];
}
}
z->num -= 1;
}
参考资料:http://blog.chinaunix.net/uid-20196318-id-3030529.html