B树学习梳理

一颗 M B T ,满足以下条件
1. 每个结点至多拥有 M 课子树
2. 根结点至少拥有两颗子树
3. 除了根结点以外,其余每个分支结点至少拥有 M/2 课子树
4. 所有的叶结点都在同一层上
5. k 课子树的分支结点则存在 k-1 个关键字,关键字按照递增顺序进行排序
6. 关键字数量满足 ceil(M/2)-1 <= n <= M-1
B树和二叉树一样,也属于搜索树,是多叉搜索树,M阶的意思是只每个节点做多可以有M个子树

 B树的搜索:

1.先在节点内部从小到大开始搜索元素

2.如果命中,搜索结束

3.如果未命中,再去对应的子节点搜索元素,重复步骤1

B树的代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <assert.h>

#define DEGREE		3
typedef int KEY_VALUE;

typedef struct _btree_node {
	KEY_VALUE *keys; 
	struct _btree_node **childrens;  // 子节点 等价于struct _btree_node *childrens[M]  (M代表M阶)
	int num;  // 该节点key的个数  (需要注意的是子节点childrens会比keys的数量大1)
	int leaf; // 是否为叶子节点
} btree_node;

typedef struct _btree {
	btree_node *root;
	int t; // t:M阶的一半,之所以这么处理,方便代码的编写
} btree;

// t:M阶的一半,之所以这么处理,方便代码的编写
// leaf: 创建的节点是否为叶子节点
btree_node *btree_create_node(int t, int leaf) {

	// 申请一个节点btree_node的内存空间
	btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));
	if (node == NULL) assert(0);

	node->leaf = leaf;
	// 申请节点的keys的内存空间 (对应第五条:有k课子树的分支结点则存在k-1个关键字)
	node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
	// 申请节点的子节点的内存空间,需要注意的是,子节点childrens的类型是_btree_node **二级指针
	// childrens是一个指针,指向的内存空间为struct _btree_node *的首地址,所以在申请的内存的时候,需要以btree_node*为单位
	// 和btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));的申请不同
	node->childrens = (btree_node**)calloc(1, (2*t) * sizeof(btree_node*));
	// 初始子节点个数
	node->num = 0;

	return node;
}

// 节点的销毁
void btree_destroy_node(btree_node *node) {

	assert(node);

	free(node->childrens);
	free(node->keys);
	free(node);
	
}

// t:M阶的一半,之所以这么处理,方便代码的编写
void btree_create(btree *T, int t) {
	T->t = t;
	
	// 创建根节点 为叶子节点
	btree_node *x = btree_create_node(t, 1);
	T->root = x;
}

 B树的添加:

1.新添加的元素必定是添加到叶子节点

2.当节点的子节点在达到M-1的时候,会出现节点的分裂,中间的节点上溢到父节点,两边的节点进性分裂

3.如果上溢后,导致父节点个数达到M-1后,会继续进性分裂,如果最终传递到根节点,且根节点也达到了M-1个,那么根节点分裂,根节点的最中间的成为新的根节点

// 分裂函数
// T: B树  x:要分裂的节点的父节点 i:要分裂的节点在父节点中的位置
void btree_split_child(btree *T, btree_node *x, int i) {
	int t = T->t;

	// y为要分裂的节点的地址
	btree_node *y = x->childrens[i];
	// 创建新的节点 用于分裂使用
	// 是否为叶子节点需要和y保持一致,因为新分裂的节点和y为同一层
	btree_node *z = btree_create_node(t, y->leaf);

	// 新分裂的节点的个数为 M阶的一半减一
	// 已6阶为例,五个keys的前两个和后两个分裂,中间的进行上溢
	// 所以新增节点个数为2
	z->num = t - 1;

	// 将原节点的后两个节点赋值给新节点的前两个节点
	int j = 0;
	for (j = 0;j < t-1;j ++) {
		z->keys[j] = y->keys[j+t];
	}
	// 如果不是叶子节点 需要维护子节点
	if (y->leaf == 0) {
		for (j = 0;j < t;j ++) {
			z->childrens[j] = y->childrens[j+t];
		}
	}

	// 更新分裂后原节点的节点个数 以及节点的
	y->num = t - 1;
	// 将父节点的子节点的后移,为分裂后上溢的元素腾出位置
	for (j = x->num;j >= i+1;j --) {
		x->childrens[j+1] = x->childrens[j];
	}

	// 分裂后的父节点的子节点新增节点的指向更新
	x->childrens[i+1] = z;

	// 父节点的keys后移,为分裂后上溢的元素腾出位置
	for (j = x->num-1;j >= i;j --) {
		x->keys[j+1] = x->keys[j];
	}
	// 分裂后的keys更新
	x->keys[i] = y->keys[t-1];
	// 分裂后的父节点num更新
	x->num += 1;
}

// T: B树  x:要插入的节点
void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {

	int i = x->num - 1;

	if (x->leaf == 1) {
		// 叶子节点插入时候 遍历查找插入位置 找到后进行插入
		while (i >= 0 && x->keys[i] > k) {
			x->keys[i+1] = x->keys[i];
			i --;
		}
		x->keys[i+1] = k;
		x->num += 1;
		
	} else {
		// 非叶子节点,需要先找到要插入的子树的位置
		while (i >= 0 && x->keys[i] > k) i --;
		
		// 如果找到的子树已满,那么先进行分裂
		if (x->childrens[i+1]->num == (2*(T->t))-1) {
			btree_split_child(T, x, i+1);
			if (k > x->keys[i+1]) i++;
		}
		
		// 分裂后在进行插入
		btree_insert_nonfull(T, x->childrens[i+1], k);
	}
}

// B树的插入
void btree_insert(btree *T, KEY_VALUE key) {
	btree_node *r = T->root;
	// 如果根节点已满
	if (r->num == 2 * T->t - 1) {
		
		// 创建新的根节点
		btree_node *node = btree_create_node(T->t, 0);
		// 根节点指向新创建的节点
		T->root = node;
		// 新的根节点的子节点指向原来的根节点
		// 新创建的根节点的keys没有任何的值,只是方便下面的上溢操作统一处理
		node->childrens[0] = r;
		
		// 由于原来的根节点为node节点的第一个子节点
		// 所以要进性分裂,具体的分裂注释可参考btree_split_child函数
		btree_split_child(T, node, 0);

		int i = 0;
		// 分裂后,对比key 如果大于第一个子节点的key,那么新增的节点要在父节点的第二个位置的子节点上
		// 如果小于第一个子节点的key ,那么需要插在第一个子节点的位置
		if (node->keys[0] < key) i++;
		// 节点不满的时候的插入函数,参考该函数的注释
		btree_insert_nonfull(T, node->childrens[i], key);
	} else {
		btree_insert_nonfull(T, r, key);
	}
}

B树的遍历:

// B树节点的遍历 方式1
void btree_traverse(btree_node *x) {
	int i = 0;

	// 遍历x的所有子节点
	for (i = 0;i < x->num;i ++) {
		// 如果x为非叶子节点,递归遍历其子节点
		if (x->leaf == 0) 
			btree_traverse(x->childrens[i]);
		// 访问节点的key
		printf("%C ", x->keys[i]);
	}
	
	// 由于childrens的数量会比keys的数量大1
	// 所以要单独在进行递归遍历 因为无法执行printf("%C ", x->keys[i]);
	if (x->leaf == 0) btree_traverse(x->childrens[i]);
}

// 遍历方式2
// 打印B树 的node节点 传入根节点时候,打印整颗树
void btree_print(btree *T, btree_node *node, int layer)
{
	btree_node* p = node;
	int i;
	if(p){
		printf("\nlayer = %d keynum = %d is_leaf = %d\n", layer, p->num, p->leaf);
		// 打印node的keys
		for(i = 0; i < node->num; i++)
			printf("%c ", p->keys[i]);
		printf("\n");
#if 0
		printf("%p\n", p);
		// 打印node的childrens 地址
		for(i = 0; i <= 2 * T->t; i++)
			printf("%p ", p->childrens[i]);
		printf("\n");
#endif
		layer++;
		// 递归遍历子节点
		for(i = 0; i <= p->num; i++)
			if(p->childrens[i])
				btree_print(T, p->childrens[i], layer);
	}
	else printf("the tree is empty\n");
}

B树的删除:

1.从根节点遍历查找目标key (需要注意的是,每层遍历的时候,都会去检测改层的节点是否小于M/2, 如果小于,就会做一些合并或者借取的操作,这样保证了在实际删除的时候,可以正常的借取或者合并的操作)

2.找到目标节点

        a.为叶子结点,直接删除

        b.不为叶子节点,判断左右子树的节点数量是否大于等于M/2

        c.有一个符合b的条件的时候,将子节点的一个节点借取到父节点被删除的节点位置,然后再              删除子节点,可以看做是删除前驱或者后继

        d.找不到满足b的情况,将子节点进行合并

        e.再次调用删除,删除合并后的节点

3.没有找到目标值,目标值存在子树中,处理子树

4.如果子树的数量大于等于M/2,直接调用递归删除

5.如果子树的数量不满足4,先看左右子树是否可以借一个节点,可以的话借取后,再次调用删除

6.左右子树不能借节点,需要合并左或者右子树,然后再次调用删除

// 合并节点
// node:要合并的两个节点的父节点
// idx:要合并的节点的第一个子节点的索引
void btree_merge(btree *T, btree_node *node, int idx) {

	btree_node *left = node->childrens[idx];
	btree_node *right = node->childrens[idx+1];

	int i = 0;

	/data merge
	// 将父节点的key拿下来 合入左子树
	left->keys[T->t-1] = node->keys[idx];
	
	// 将右子树合入左子树
	for (i = 0;i < T->t-1;i ++) {
		left->keys[T->t+i] = right->keys[i];
	}
	
	// 如果左子树不是叶子节点,需要将右子树的子节点也合入左子树
	if (!left->leaf) {
		for (i = 0;i < T->t;i ++) {
			left->childrens[T->t+i] = right->childrens[i];
		}
	}
	left->num += T->t;

	//destroy right
	btree_destroy_node(right);

	// 更新父节点的子树指向信息 以及keys的信息
	for (i = idx+1;i < node->num;i ++) {
		node->keys[i-1] = node->keys[i];
		node->childrens[i] = node->childrens[i+1];
	}
	node->childrens[i+1] = NULL;
	node->num -= 1;

	if (node->num == 0) {
		T->root = left;
		btree_destroy_node(node);
	}
}

// B树节点的删除
// node: 当前遍历的节点
// key要删除的目标值
void btree_delete_key(btree *T, btree_node *node, KEY_VALUE key) {

	if (node == NULL) return ;

	int idx = 0, i;

	// 查找keys中第一个不小于key(等于或大于key,如果第一个符合要求的是大于,说明没有找到目标)的索引 idx
	// 如果没找到的时候,idx为num
	while (idx < node->num && key > node->keys[idx]) {
		idx ++;
	}
	
	// 如果idx >= node->num 说明没0找到key
	// 如果idx < node->num 但key != node->keys[idx] 也说明没找到
	if (idx < node->num && key == node->keys[idx]) {
		
		// 如果是叶子节点,直接删除
		if (node->leaf) {
			// 直接将要删除的后面的keys前移覆盖
			for (i = idx;i < node->num-1;i ++) {
				node->keys[i] = node->keys[i+1];
			}
			
			// 前移后,将最后一个key置空
			node->keys[node->num - 1] = 0;
			// 删除后 num减一
			node->num--;
			
			// B树性质3. 除了根结点以外,其余每个分支结点至少拥有M/2课子树
			// 可得出,如果为0,说明node为根节点
			if (node->num == 0) { //root
				free(node);
				T->root = NULL;
			}

			return ;
		} 
		// 不是叶子节点 当前节点的被删除目标的左子树节点大于等于 M/2
		// 找前驱 然后删除
		else if (node->childrens[idx]->num >= T->t) {
			
			// 找到被删除目标节点的左子树
			btree_node *left = node->childrens[idx];
			// 将左子树中最大的值赋给给删除的位置的key 
			// 相当于删除了当前的目标节点,然后将实际删除节点替换为子节点
			node->keys[idx] = left->keys[left->num - 1];
			
			// 递归调用 删除前驱
			btree_delete_key(T, left, left->keys[left->num - 1]);
		} 
		// 不是叶子节点 当前节点的被删除目标的右子树节点大于等于 M/2
		// 找后继 然后删除
		else if (node->childrens[idx+1]->num >= T->t) {
			// 和上面的情况相同,只不过是右子树
			btree_node *right = node->childrens[idx+1];
			node->keys[idx] = right->keys[0];

			btree_delete_key(T, right, right->keys[0]);
			
		} 
		// 左右子树都节点都不够,将左右子树进行合并,然后删除合并后的节点
		else {

			btree_merge(T, node, idx);
			btree_delete_key(T, node->childrens[idx], key);
			
		}
		
	} 
	// 当前节点没有找到被删除的目标key
	// 
	else {
		
		// 被删除的目标节点可能会存在子节点中
		btree_node *child = node->childrens[idx];
		if (child == NULL) {
			printf("Cannot del key = %d\n", key);
			return ;
		}
		
		// 如果子节点的节点数过少
		if (child->num == T->t - 1) {

			// 被删除的目标key的左右子树
			btree_node *left = NULL;
			btree_node *right = NULL;
			if (idx - 1 >= 0)
				left = node->childrens[idx-1];
			if (idx + 1 <= node->num) 
				right = node->childrens[idx+1];

			// 左子树或右子树可借
			if ((left && left->num >= T->t) ||
				(right && right->num >= T->t)) 
			{

				int richR = 0;
				if (right) richR = 1;
				if (left && right) richR = (right->num > left->num) ? 1 : 0;
				
				// 右子树可借
				if (right && right->num >= T->t && richR) 
				{ //borrow from next
					// 子节点借取后,num会加1,将父节点key的赋给新增的key
					child->keys[child->num] = node->keys[idx];
					// 子节点借取后,子树也会新增一个 ,由于是借的右子树的节点,所以要指向右子树的子树
					child->childrens[child->num+1] = right->childrens[0];
					child->num ++;
					
					// node的key给了child 借取的右子树的节点需要替换掉node  这个相当于树的旋转,可参考下方的截图
					node->keys[idx] = right->keys[0];
					// 由于右节点借出去一个,所以需要更新节点信息
					for (i = 0;i < right->num - 1;i ++) {
						right->keys[i] = right->keys[i+1];
						right->childrens[i] = right->childrens[i+1];
					}
					right->keys[right->num-1] = 0;
					right->childrens[right->num-1] = right->childrens[right->num];
					right->childrens[right->num] = NULL;
					right->num --;
				} 
				// 参考上面的右节点逻辑
				else { //borrow from prev

					for (i = child->num;i > 0;i --) {
						child->keys[i] = child->keys[i-1];
						child->childrens[i+1] = child->childrens[i];
					}

					child->childrens[1] = child->childrens[0];
					child->childrens[0] = left->childrens[left->num];
					child->keys[0] = node->keys[idx-1];
					
					child->num ++;

					node->key[idx-1] = left->keys[left->num-1];
					left->keys[left->num-1] = 0;
					left->childrens[left->num] = NULL;
					left->num --;
				}

			} 
			// 左子树右子树均不可借
			else if ((!left || (left->num == T->t - 1))
				&& (!right || (right->num == T->t - 1))) 
			{

				if (left && left->num == T->t - 1) {
					btree_merge(T, node, idx-1);					
					child = left;
				} else if (right && right->num == T->t - 1) {
					btree_merge(T, node, idx);
				}
			}
		}
		
		// 调用删除子节点中的目标key
		btree_delete_key(T, child, key);
	}
	
}


int btree_delete(btree *T, KEY_VALUE key) {
	if (!T->root) return -1;

	btree_delete_key(T, T->root, key);
	return 0;
}

删除逻辑核心步骤分析:

向兄弟节点借取类似树的旋转

 非叶子节点的删除,可以理解为找到其前驱或者后继,然后覆盖删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值