数据结构与算法学习01B-tree & B+ tree

B-tree B+tree

我们经常说做人要有B树,今天总结一下了解到的 B树和B+树,B-tree是B树,没有B-树。
首先我们来聊一聊,为什么要用B树,B树是一颗平衡查找树,旨在帮助我们减少磁盘I/O的开销,因为磁盘I/O的速度是很慢的,而内存中工作的速度就快很多。比如现在有 1M个4K 的文件,也就是4G, 如果构造一个1024叉B+树,只需要通过2次索引,就能找到我们要的文件的位置,也就是只需要做一次IO查找(磁盘寻址)操作。这无疑大大的提高了效率。

B树的性质

要学习B树,首先要了解一下B树的性质,B树是平衡多路搜索树。
一颗 M 阶 B树
1 每个结点上至多拥有M颗子树
2 根结点至少拥有两颗子树
3 除了根结点以外,其余每个分支结点至少拥有 M/2颗子树
4 所有的叶子节点都在同一层上 (这条性质就说明了B树是平衡的树)
5 有K颗子树的分支结点则存在 K-1 个关键字,关键字按照递增顺序进行排序
6 关键字数量满足 ceil(M/2)-1 <= n <= M-1 (ceil为上取整)
B数的层高为 logM(Num/M) M阶数,Num是结点总数量

B树和B+树的区别

B树是所有的结点都存储数据,B+树是只有叶子结点存储数据,内结点只做索引用。B树在访问内结点时要把磁盘上的数据读倒内存中,而B+树只需要内存中存放索引值即可,在内存中索引完了再做磁盘IO。

B树的操作

B树的定义

typedef int KEY_AVLUE;

struct _btree_node {
	KEY_AVLUE *keys;                   // 根结点
	struct _btree_node** childerns;	   // 子结点
	int nums;                          // 现在存储了几个结点
	int leaf;						   // 是否是叶子结点  用 int 是为了内存对齐 
}btree_node ;

struct _btree
{
	struct btree_node *root;           // 一颗 b 树有很多结点 
	int t;                             // 2*t 阶 B树
}btree;

B树添加结点

添加结点 - 先分裂(子树满了),再添加
添加的时候,永远添加的都是叶子结点
分裂:(M应当选择一个偶数,则Key(M-1)就是一个奇数,奇数就便于找中间结点做分裂)
分裂有两种情况

  1. 只有根结点的时候 ,根分裂为 根左子树右子树
    B树插入
  2. 其他情况,把自己分裂为两个节点,并把中间结点给根结点
    B树插入
// 分配内存,创建结点
btree_node *btree_create_node(int t, int leaf) {
	// 根结点
	btree_node *node = (btree_node*)calloc(1, sizeof(btree_node)); // 使用calloc是为了让它自动清零
	if (node == NULL) assert(0);
	node->leaf = leaf;
	// 有 2*t-1 个查找值
	node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
	// 可以插入的2*t个位置(子树)
	node->childrens = (btree_node**)calloc(1, (2*t) * sizeof(btree_node*));
	node->num = 0;

	return node;
}

// 分裂 x 的 第 i 个子结点
void btree_split_child(btree *T, btree_node *x, int i) {
	int t = T->t;
	// y 是 x 的第 i 个子结点,要分裂的结点
	btree_node *y = x->childrens[i];
	// z 是分裂的新结点,若y为叶子结点,则分裂的也为叶子结点
	btree_node *z = btree_create_node(t, y->leaf);
	// 存放前 t-1个值,比如 t=3 就是6阶树,有5个关键字,分裂的时候,把后2个放到z中,中间的放父节点上
	z->num = t - 1;

	// z 放后面t-1 个
	int j = 0;
	for (j = 0;j < t-1;j ++) {
		z->keys[j] = y->keys[j+t];

	// 如果 y 不是叶子结点,则把 y 的孩子也拷贝
	if (y->leaf == 0) {
		for (j = 0;j < t;j ++) {
			z->childrens[j] = y->childrens[j+t];
		}
	}
	// 把 x 的子结点从 i 开始往后移一个
	y->num = t - 1;
	for (j = x->num;j >= i+1;j --) {
		x->childrens[j+1] = x->childrens[j];
	}
	// 把空出来那个给新的分裂的结点
	x->childrens[i+1] = z;

	// 父结点的key也往后移一个
	for (j = x->num-1;j >= i;j --) {
		x->keys[j+1] = x->keys[j];
	}
	// 空出来的给 分裂结点的中间结点
	x->keys[i] = y->keys[t-1];
	x->num += 1;
}

// 根结点没满,从 x 位置开始找,插入k
void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {
	int i = x->num - 1;
	
	if (x->leaf == 1) {// 如果 x 是叶子结点
		// 找到比k小的位置i,i后的key 后移,k插到 i+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 --;
		// 找位置,插到 i+1
		// 如果要插入的树满了,就要先分裂
		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);
	}
}

// 插入操作
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;
		node->childrens[0] = r;
		btree_split_child(T, node, 0);
		// 添加到分裂后的第 i 个位置
		int i = 0;
		if (node->keys[0] < key) i++;
		btree_insert_nonfull(T, node->childrens[i], key);
	} else {
		btree_insert_nonfull(T, r, key);
	}
}

B树删除结点

删除结点 -
删除叶子结点
先借位或者合并使树成为一个可以删除叶子结点的状态,再删除。
删除内结点
先把内结点下沉到叶子结点,再删除。
借位:因为第 idx 位置的 子树的个数要 >= ceil(M/2)-1 所以 要从
idx-1 借位
idx+1 借位
先从前面借,再向后面借
合并, 合并的是
{childs[idx].keys}, {keys[idx]}, {childs[idx+1].keys}
删除的时候,永远删除的都是叶子结点
删除结点

// 释放结点内存
void btree_destroy_node(btree_node *node) {
	assert(node);
	free(node->childrens);
	free(node->keys);
	free(node);
}

// 合并
//{node->child[idx], node[idx], node->child[idx+1]} 
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;

	//合并
	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;

	//删除 right
	btree_destroy_node(right);

	// node 的后面往前移动一位
	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;

	// 把根结点合并到 左根右 了,就把这个子树提升为根结点,这种情况是 只有2个子结点和一个根结点的时候
	if (node->num == 0) {
		T->root = left;
		btree_destroy_node(node);
	}
}

// 删除结点
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 { // 借位,使删除后任然平衡

		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) { //borrow from next
					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->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);
				}
			}
		}

		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;
}

思考:图片存储索引组件(FastDFS如何实现的?)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值