B树,你有么?那么B+和B*树呢?

前言

我对B树这个东西一直不是很理解,今天就这篇博客,搞懂B树的来龙去脉。

B树

B树英文B-tree,有的人居然翻译成B-树,我也是醉了。其实这就是个多叉搜索树,而二叉搜索树是它的特化情况。B树里存放的是key-value的键值对,只不过是根据key进行的组织排序而已。
一颗m阶B树特性如下:
1.任意一个节点的孩子数量最大是m;
2.除了根节点root和叶子结点外,其他的节点孩子数量是ceil(m / 2),ceil是向上取整。
3.根节点可以最少有两个孩子,当然如果只有一个根节点的话咱们不讨论
4.每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它
5.所有叶子结点都位于同一层
你可能粗略的看完一遍B树特性,感觉很奇怪。放心,当你看完B树的构造方法和删除方法时,你就释然了。

B树的插入操作

1.根据要插入的key的值,找到叶子结点并插入
2.插入完成后,判断当前结点key的个数是否小于等于m-1,若满足则结束,否则进行第3步
3.以结点中间的key为中心分裂成左右两部分,然后将这个中间的key插入到父结点中,这个key的左子树指向分裂后的左半部分,这个key的右子支指向分裂后的右半部分,然后将当前结点设置成父结点,继续进行第2步
其实看完了插入操作,B树的很多特性就都得到了解释,尤其是特性2和特性5,就显得一目了然了。我这么说吧,除了根节点和叶子结点之外,其他节点最差也是分裂后的一半,所以特性2就很好理解。另外,你难道不觉得,B树的增长方向,其实是向上增长么???这么一来,特性5也就非常好理解。

B树的删除操作

1.如果当前需要删除的key位于非叶子结点上,则用后继key(这里的后继key均指后继记录的意思)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。此时后继key一定位于叶子结点上,这个过程和二叉搜索树删除结点的方式类似。删除这个记录后执行第2步
2.该结点key个数大于等于Math.ceil(m/2)-1,结束删除操作,否则执行第3步
3.如果兄弟结点key个数大于Math.ceil(m/2)-1,则父结点中的key下移到该结点,兄弟结点中的一个key上移,删除操作结束。否则,将父结点中的key下移与当前结点及它的兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个孩子指针就变成了一个孩子指针,指向这个新结点。然后当前结点的指针指向父结点,重复上第2步
其实理解了插入操作后再回过头来看看删除操作,就显得格外简单,就是个逆操作。

B树的查询操作

查询操作就十分的简单了,我就不写了,仿照二叉树的遍历方式就可以了,大差不差的就行。

B树的代码实现

上代码!(只提供部分好吧,剩下的各位小伙伴加油!)

//0.一些基本的宏定义
#define M	3
//1.定义B树节点结构
struct BTreeNode {		
	std::list<std::pair<int, void*> > KeyValue;		//索引Key-Value数组,最大M-1
	std::list<struct BTreeNode*> childs;	//孩子们,最大M
	struct BTreeNode* parent = nullptr;
};

解释一下为啥要这么搞。首先B树里插入的是keyValue这样的键值对,因此用list存放这些pair。因为经常会有插入删除操作,所以我用户list。childs是指向孩子节点的list。最后还有一个parent方便咱们实现算法。
然后是BTree结构:

class BTree
{
private:
	struct BTreeNode* rootNode = nullptr;
};

还需要定义一些基本的操作:

private:
	//是否是叶子结点
	bool isLeafNode(struct BTreeNode* node);
	//节点的最大key
	int maxKey(struct BTreeNode* node);
	//节点的最小key
	int minKey(struct BTreeNode* node);
	//将node分裂成两个node,同时返回新的node指针和一组key_value
	std::pair<int, void*>splitNode(struct BTreeNode* node, struct BTreeNode* newNode);
	//将一组key_value插入到node中,同时更新插入后的node的childs
	void upInsertNode(struct BTreeNode* node, std::pair<int, void*> KV, struct BTreeNode* splitChildNode, struct BTreeNode* newChildNode);

其中分裂操作需要注意的是,需要根据中间值将KeyValue链表和Childs链表都做一分为二。
其中向上插入操作需要注意的是,跟新KeyValue链表很简单,而更新Childs链表时,一定是某一个child后面添加一个新的newSplitNode。
删除操作与之类似,过分简单,就不写了。

为何是B树而不是二叉树

在一些文件系统或者数据库的索引中,一般使用的是树形结构(如B树和B+树),为啥不用hash或者二叉树呢?
首先,hash的话好的情况下是O(1)的复杂度很棒,但是哈希冲突是难免的,数据量上去以后会有很大的哈希冲突的。
而二叉树的搜索时间复杂度确实要好于等于B树,可是咱们还得考虑磁盘操作。毕竟文件系统或者数据索引,其内容最后都是放在磁盘上的,而操作一次磁盘消耗的时间可就非常大了(至少比内存大很多),同样的数据量下,二叉树的高度(可能的切换磁道次数)要比B树多,这就是为啥用B树而不用二叉树的原因。

B+树

B+树是在B树的基础上发展来的,对B树做了进一步的优化。B+树中的节点分为内部节点和叶子结点,其中内部节点不保存数据value,只保存key用作索引,B+树的所有数据都在叶子结点上,且每个叶子节点都用链表链接在一起的。
相较于B树,B+树的优点在于:
1.内部节点因为不存放value,因此如果把所有同一内部节点的关键字放在同一块磁盘中,盘块所能容纳的关键字数量也就越多,一次性读入内存中的需要查找的关键字也就越多,相对IO读写次数降低;
2.稳定。B树咱们不清楚确定的访问磁盘次数,而B+树是确定的,因为数据一定在叶子结点上。
3.对于一些范围搜索,B+树更为强大,因为B+树的所有叶子结点都用链表串联在一起。如果我想找12~20之间的key值对应的value,那么B树要用中序遍历,很麻烦,而B+树只需要找到12,然后下面就是链表操作了,非常方便。

B+树的插入删除操作

与B树有一些区别,当溢出时,中间值仍可保留在叶子结点中。

B*树

其实B树就是B+树的再一次改版。它将内部节点之间同层的也用链表串联在一起了。
这样有什么好处呢?就是当节点做插入操作时,如果发现此节点要溢出,则:
1.B+树求助它的父亲节点
2.B
树先求助它的兄弟节点,看看它的兄弟能否帮助其分担一下,如果可以,那就很棒;如果不可以,则将自己和兄弟,每个人各拿出三分之一,构成一个新的节点,然后再去父节点中增加新的指针。
B*树这样做的好处是,提高了节点的最低利用率,由B+树的二分之一,提高到了三分之二(因为只要是产生节点,就一定有三分之一+三分之一的节点数量)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值