详谈B树的增加与删除

多叉树

  1. 相较于二叉树,层高更低=>节点数量更少=>减少查找节点数量数(寻址,读磁盘)
  2. 没有约束平衡
  3. 没有约束子树数量
  4. 数据有规律

B树

减少寻址
约束平衡
约束每个节点有多个子树
比如:
一颗M阶B数T,

  1. 每个节点 最多M颗子树
  2. 根节点至少两颗子树,其他节点必须多余两颗子树
  3. 除了根结点以外,其余每个分支结点至少拥有M/2课子树
  4. 所有的叶结点都在同一层上
  5. 有k课子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序
  6. 关键字数量满足ceil(M/2)-1 <= n <= M-1

B+树

基于B树,对于所有叶子节点,通过前后指针链接到一起
基于B树,所有数据存到叶子节点上
基于B树, 需要增加父节点

100w数据 mysql如何存

一个节点用1页(4K),可以最大程度的利用磁盘读写效率

B树代码中的两个概念

  1. 分裂
    插入数据时候为了满足
  2. 合并

B树创建

typedef int KEY_VALUE;

typedef struct _btree_node {
	KEY_VALUE *keys;
	struct _btree_node **childrens;
	int num; //关键字数量
	bool leaf;//是否是叶子节点
} btree_node;

typedef struct _btree {
	btree_node *root;
	int t;
} btree;

btree_node *btree_create_node(int t, int leaf) {

	btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));
	if (node == NULL) assert(0);

	node->leaf = leaf;
	node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
	node->childrens = (btree_node**)calloc(1, (2*t) * sizeof(btree_node));
	node->num = 0;

	return node;
}
// M阶,t为M/2
void btree_create(btree *T, int t) {
	T->t = t;
	
	btree_node *x = btree_create_node(t, 1);
	T->root = x;
	
}

B树添加

假设这是一颗6阶的B树
根据B树性值得到以下信息

  1. 每个节点最多6颗子树
  2. 关键字数量为2~5
  3. 除了根节点,其他节点最少3颗子树,2个关键字

现在开始依次插入ABCDEFGHI九个节点
在这里插入图片描述
可以看出,根节点与非根节点的插入数据方式并不相同并不相同
在该节点已满又要插入数据时候,需要分裂
且分裂时候可能会用到父节点
因此定义分裂函数

分裂

T: 哪颗树
x: 父节点
i: 分裂第几颗子树
void btree_split_child(btree *T, btree_node *x, int i) {
	int t = T->t;

//找到需要分裂的子树
	btree_node *y = x->childrens[i];
//创建一颗子树用以取代y树右半部分,以完成分裂
	btree_node *z = btree_create_node(t, y->leaf);

//复制右半部分资源
//t 为M/2 ,M为子树个数,分裂后一边一半,所以y、z子树个数均为t, 关键字个数为t-1
	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];
	}
	for (j = x->num-1;j >= i;j --) {
		x->keys[j+1] = x->keys[j];
	}

//插入新节点
	x->childrens[i+1] = z;
// 原y树中间的关键字放到父节点中用以区分原y树的左右
	x->keys[i] = y->keys[t-1];

	x->num += 1;
	
}

插入节点

上面已经解释,添加节点时候,节点满,我们便将之分裂,造成一个不满的节点,随后添加进入,换言之,我们始终是将新key加入到一个不满的节点中

  1. 寻找到位置,未满,直接插入
  2. 寻找到位置,处于内节点,已满,分裂内节点
  3. 寻找到位置,处于叶子节点,已满,分裂叶子节点

实际实现时候,最终都是插入到叶子节点, 因为插入叶子节点,对于多叉树代码是最为方便快捷的方式。

void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {

	int i = x->num - 1;
//基于上述文案讲述情况,插入关键字,需要追溯该树到叶子节点。此处我们决定使用回溯算法
	if (x->leaf == 1) {
	//追溯到了叶子节点,找到key对应的位置,插入key
		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++;
		}
	//继续向下追寻key对应的位置
		btree_insert_nonfull(T, x->childrens[i+1], k);
	}
}

从根节点处开始添加key

上面说到,为了符合B树性质,在很多时候需要分裂节点,分裂节点是父子节点之间的互动, 那么在根节点满的情况下,新增节点时候,为了使得代码简便统一,此处增加一个节点用以填充父节点。

void btree_insert(btree *T, KEY_VALUE key) {
	//int t = T->t;

	btree_node *r = T->root;
	if (r->num == 2 * T->t - 1) {
		//如果根节点已满,虚拟父节点
		btree_node *node = btree_create_node(T->t, 0);
		T->root = node;

		node->childrens[0] = r;

		//分裂原父根节点
		btree_split_child(T, node, 0);

		int i = 0;
		//找到key应该插入的子树
		if (node->keys[0] < key) i++;
		//插入key
		btree_insert_nonfull(T, node->childrens[i], key);
		
	} else {
		//根节点未满, 直接插入key
		btree_insert_nonfull(T, r, key);
	}
}

B树的删除

为了方便操作,需要将需要删除的节点移动到最叶子节点处,进行处理,如此便不需要再去考虑如何整理删除对应关键字时候遗留下来的子树。

为了下沉关键字,且为了满足性值关键字数量满足ceil(M/2)-1 <= n <= M-1 ,做出如下分析:

  1. 关键字 的 子节点关键字数均为M/2-1 :直接合并
  2. 关键字 的 子节点的关键字数均大于M/2-1 ,向其借一位关键字。

合并

该变化中,其左右子树的关键字均为M/2-1,加上父节点对应的关键字,刚好满足M-1的关键字上限要求

//合并T数中, node->keys[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;
//此处借左子树空间
//父节点下沉,放置在关键字buffer的中间。
	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;

//删除脏数据右子树
	btree_destroy_node(right);

//重新整理父节点的buffer空间,原父关键字右边的统统左移一格
	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;

//如果是关键字数为1的,则该节点为父节点
	if (node->num == 0) {
		T->root = left;
		btree_destroy_node(node);
	}
}

寻找并删除

上面讲到, 为了删除节点,需要将节点不断下降到叶子节点上去删除。
所以删除操作将会遍历到B树的所有层上

那么此处回溯自然是最优选择

void btree_delete_key(btree *T, btree_node *node, KEY_VALUE key) {

	if (node == NULL) return ;

	int idx = 0, i;

//从前往后寻找节点处于什么位置。
	while (idx < node->num && key > node->keys[idx]) {
		idx ++;
	}

	if (idx < node->num && key == node->keys[idx]) {
//如果在该层找到了该节点,判断是否是叶子节点
		if (node->leaf) {
			//如果是叶子节点,将之删除
			for (i = idx;i < node->num-1;i ++) {
				node->keys[i] = node->keys[i+1];
			}

			node->keys[node->num - 1] = 0;
			node->num--;
			
			if (node->num == 0) { //root
				free(node);
				T->root = NULL;
			}

			return ;
		} else if (node->childrens[idx]->num >= T->t) {
//在找到了想要删除的节点的情况下, 
//发现该节点不处于叶子节点,
//且它的关键字数较多,至少多余最低限度,
//那么就将之直接删除,
//然后从其左节点中找到最大的节点或者右节点最小的替换上来
			btree_node *left = node->childrens[idx];
			node->keys[idx] = left->keys[left->num - 1];

			btree_delete_key(T, left, left->keys[left->num - 1]);
			
		} 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);
			
		}
		
	} else {
//如果没有在该行找到该节点
//那么要不该层所有数据都比key小
//要么已经能够定位到key所在的子树
//由于删除时候是一层一层向下查找,第一种情况可以说明key不存在
		btree_node *child = node->childrens[idx];
		if (child == NULL) {
			printf("Cannot del key = %d\n", key);
			return ;
		}

		if (child->num == T->t - 1) {
//如果下一层子树长度为最小长度
			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) {
//将该层节点下移
					child->keys[child->num] = node->keys[idx];
					child->childrens[child->num+1] = right->childrens[0];
					child->num ++;

//其兄弟树的最小关键字替换到当前行的当前位置
					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->keys[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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值