树简介
什么是B树
B树(Balanced Tree)是一种优秀的数据结构,用一种通俗的话来说:B树是一种一个节点可拥有多于两个子节点的树
B树有两种衡量标准:阶,度。
t度 的B树就是 2t阶 的B树(这也是B树的分裂机制决定的)
因为t度的B树节点最多有2t个孩子,2t-1个关键字;m阶的B树最多有m个孩子,
其实通过度定义的B树和通过阶数定义的B树,区别就是一个是用的这个B树节点的最小度数一个是用的这个树节点的最大度数。
这里我们统一使用阶来描述树。
一个t阶B树有以下约束:【注意,所有的除法都向上取整】
- 根节点除外,每个节点最少有t/2个孩子,最多有t个孩子【也就是说一个节点最少有t/2-1个值,最多有t-1个值,根节点除外】
- 根节点至少有两个孩子
- 同一节点x[i],x[i+1]之间的指针指向的子树的值必须在x[i],x[i+1]之间
B树的实例
比较有名的B树有2-3树,2-3-4树。
- 其中2-3树是度数为1的树,即最多有三个子树,也就是每个节点可以放1,2个值
- 2-3-4树是度数为2的树,最多有四个子树,每个节点可以放1,2,3个值
B树,B-树的联系和区别
B树和B-树是一个东西,会出现这两种说法只是翻译时出的意外
B树的英文名称是B-Tree。有的人在翻译时把横线翻译成连接符,所以就翻译成了B树。有的人认为是减号,就翻译成了B-树,正好还有B+,B*,所以即便翻译成B-也还凑合着说的过去,所以想叫什么就看自己喽。
B树,B+树的区别和联系
B+树是对B树的一种变形树,它与B树的差异在于:
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点是:
- 由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
- B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
B+树和B*树的却别和联系
B*树是B+树的变体,在B+树的基础上,B*树中非根和非叶子结点再增加指向兄弟的指针;B*树定义了阶为M的树非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)
平衡树实现的思考
平衡树的出现是因为。搜索树的分布一般情况下很不平衡,导致搜索最坏情况下的搜索树深度可能达到O(n)。而二叉搜索树的结构完全由节点插入的顺序决定【他是对输入敏感的】,树不具有分配节点去向的能力,所以也无法根据当前的情况动态的改变树的高度,就容易出现极端的情况。
所以我们需要设计一种结构,能使根节点拥有对新来的子节点的动态分配权,增加根节点分配权的方法就是让他可以比,让根节点有可以权衡树平衡程度的信息
- AVL树的操作思路是先让节点插进去,然后判断,不合适就旋转,把自己转过去,增加兄弟节点的树深
- 笛卡尔树是根据额外的一个
value
通过堆的思想来决定节点的去向- Treap和笛卡尔树相似,不过它是一个单纯的BBST,所以它的
value
随机生成,用来平衡树的深度
B树不是二叉搜索树,但是你也可以把他看成二叉搜索树,它用来平衡的方法是缓存:
以2-3树为例,来了一个值后如果能放在这个节点,这个节点就先存着,等这个节点放不下了,然后再统一决定节点的去向。
树操作算法概述
B树
例子:2-3树定义
2-3树是每个非叶子节点最多有三个子节点。
- 你可以把它理解成在二叉搜索树的基础上增加了一种新的节点——里面有两个键值,有三个子树。
- 你也可以用一种更加通俗的名字叫它——3阶B树
例子:2-3-4 树定义
2-3-4树就是4阶B树。
树的操作方法
我们这里主要介绍B树的插入操作和删除操作。
搜索
之前我们写二叉树是小于就去左边找,大于就去右边找。这里在和本节点中的值比较时就从左往右顺序比较即可,找到比inputKey大的key[i]后,顺着key[i]的左子树继续向下查询。
插入
B树的插入有以下约束:
- 只有叶子节点可以插入值
- 节点溢出后进行分裂时,新构建的父节点一定要插入到原来的父节点的位置
其实这两个约束也就是我们插入B树的思路:
- 通过搜索找到合适插入的叶子节点【插入一定是在叶子节点中】
- 插入
- 判断是否溢出,溢出则分裂,将中值移动至父节点
- 判断父节点是否溢出。。。。。。。
- 判断父节点的父节点。。。。。
- 。
- 。
- 一直到根节点
引用程序员修炼之路的博客的一句话就是:
插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。
删除
讲道理,删除还真的有点复杂,这个涉及到从兄弟节点、父节点的借值。思路如下:
- 通过搜索找到要删除的值所在的节点
- 删除此值,然后使用子树的值来替换:
- 如果这个值的左子树可以借值的话【去掉一个值还满足B树的要求】,就用左子树的最右节点来代替
- 如果左子树不行,但是右子树可以的话,就用右子树的最左节点来代替
- 子树的值不能满足替换,如果它的左右兄弟节点可以,就通过【兄弟节点——相同的父节点——本节点】的方式进行借值
- 如果兄弟节点也不行,就和相近的兄弟节点合并成为一个新的节点
关键代码
直接放项目的地址了,我是用的java写的增删操作,没有用C。还有就是没有专门为这个建git,源码在com.gateway.learn.tree
下面。github地址:https://github.com/LiPengcheng1995/gateway-parent.git
插入
这里,插入操作的关键算法如下:
/**
* @Author: lipengcheng20
* @Date: 2018/9/28 14:23
* @Description: 对外暴露,用于在此节点或此节点的子节点中插入key
**/
public BTreeNode findAPlaceAndInsert(int key) {
if (this.getData().size() == 0) {
this.initNodeWithAKey(key);
return this;
}
if (this.getSubTreeNumber() == 0) {
// 是叶子节点,就在这里插了
// 此节点定不为空
this.insertKey(key);
return this.checkOverFlowAndReturn();
} else {
for (int i = 0; i < this.getKeyNumber(); i++) {
if (this.getKeyByIndex(i) > key) {
//插入到这个的左子树中
if (Objects.isNull(this.getSubTreeOnLeftOf(i))) {
//子树对应的为空,直接插进去
BTreeNode temp = new BTreeNode(this.getMAX_SUBTREE_NUMBER());
temp.initNodeWithAKey(key);
this.setSubTreeOnLeftOf(i, temp);
return this;
} else {
//对应的子树不为空,直接在子树中插,然后根据实际情况看是不是要分裂
BTreeNode temp = this.getSubTreeOnLeftOf(i).findAPlaceAndInsert(key);
if (temp.getData().size() == 3) {
//子节点做了分离
this.insertKeyByKeyIndex(i, temp.getKeyByIndex(0));
this.setSubTreeOnLeftOf(i, temp.getSubTreeOnLeftOf(0));
this.setSubTreeOnRightOf(i, temp.getSubTreeOnRightOf(0));
}
return checkOverFlowAndReturn();
}
}
}
//插入到最右边的子树
if (Objects.isNull(this.getSubTreeOnRightOf(this.getKeyNumber() - 1))) {
//最右边子树对应的为空,直接插进去
BTreeNode temp = new BTreeNode(this.getMAX_SUBTREE_NUMBER());
temp.initNodeWithAKey(key);
this.setSubTreeOnRightOf(this.getKeyNumber() - 1, temp);
return this;
} else {
//最右边对应的子树不为空,直接在子树中插,然后根据实际情况看是不是要分裂
BTreeNode temp = this.getSubTreeOnRightOf(this.getKeyNumber() - 1).findAPlaceAndInsert(key);
if (temp.getData().size() == 3) {
//子节点做了分离
this.insertKeyToTheEnd(temp.getKeyByIndex(0));
this.setSubTreeOnLeftOf(this.getKeyNumber() - 1, temp.getSubTreeOnLeftOf(0));
this.setSubTreeOnRightOf(this.getKeyNumber() - 1, temp.getSubTreeOnRightOf(0));
}
return checkOverFlowAndReturn();
}
}
}
删除
只写了伪代码,感觉有点复杂,就先不写了
B+树实现
太难太难
源码
直接放项目的地址了,我是用的java写的增删操作,没有用C。还有就是没有专门为这个建git,源码在com.gateway.common.web.learnTree
下面。github地址:https://github.com/LiPengcheng1995/try.git
树应用场景
主要是数据库的搜索和文件系统的搜索。通过多叉树,这样可以一次读取多个节点,减少换页。B+树尤其如此,B+树非叶子节点只存储了“主键”。
参考文献
https://www.cnblogs.com/vincently/p/4526560.html
https://www.cnblogs.com/hdk1993/p/5840599.html
应用:http://blog.codinglabs.org/articles/theory-of-mysql-index.html
规律:
https://blog.csdn.net/pleasecallmewhy/article/details/8451889
https://blog.csdn.net/guoziqing506/article/details/64122287