索引数据结构之B-Tree与B+Tree(上篇)

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,便可关注微信公众号,阅读更多Spring源码分析Java并发编程Netty源码系列文章。html

微信公众号

树是一种十分常见的数据结构,根据子结点的个数,咱们能够将树分为二叉树和多叉树。每一个结点最多两个子结点的树称之为二叉树,比较典型的二叉树有二叉搜索树、彻底二叉树、满二叉树、二叉平衡树、红黑树等。子结点的个数大于 2 的树称之为多叉树,常见的多叉树有 B 树和 B+树。算法

B 树和 B+树是一种多路搜索树,它由二叉搜索树演变而来,经常使用于数据库的索引结构中,且 B+树和 B 树具备不少类似的地方,也比较容易弄混,所以本文将二者放在一块儿进行学习,进行对比。数据库

B-Tree

B-Tree 又叫作 B 树,不少人见到有 B+树(B+Tree),因此常常会把 B-Tree 和 B 树当作是两种树,实际上 B-Tree 和 B 树是同一种树(单词 B-Tree 翻译过来就是 B 树)。(这个”不少人“就包括笔者,笔者是个菜鸟,最开始把 B-Tree、B 树,B+Tree 当成是三种树,还常常把它们理解为 B 减树,B 树,B 加树,后来去网上查了查才搞清楚)。编程

对于树这种数据结构,有一个描述树结构的概念叫作度(也叫作阶),它描述的是一个结点中子结点的个数,例如一个二叉树,每一个结点最多有 2 个子结点,所以二叉树的度(阶)为 2。对于 B-Tree 而言,一样也有阶的概念,例如一个 5 阶的 B-Tree,表示的是每一个结点最多有 5 个子结点。微信

对于一个阶数为 m 的 B-Tree,它有以下性质:数据结构

  1. 每一个结点最多有 m 个子结点;
  2. 每一个非叶子结点(根结点除外)至少含有 m/2 个子结点;
  3. 若是根结点不是叶子结点,那么根结点至少有两个子结点;
  4. 对于一个非叶子结点而言,它最多能存储 m-1 个关键字(所谓的关键字,咱们能够理解为就是节点上存放的数据);
  5. 每一个节点上,全部的关键字都是有序的,从左至右,依次从小到大排列;
  6. 每一个关键字的左子树的值均小于当前关键字,右子树的值均大于当前关键字;
  7. 每一个节点都存有索引和数据(记住这一点很是重要,这是和后面介绍的 B+Tree 的最重要的区别之一)。

从上面的性质来看,对于 B-Tree 的根结点而言,关键字数量的范围为 1<= k <= m-1;非根结点,关键字的范围为 m/2 <= k <= m-1。知道了这些性质,下面咱们分别看看 B-Tree 的插入、查找、删除过程。并发

1. 插入

在向一个 m 阶的 B-Tree 中插入数据时,为了保证上述 B-Tree 的性质,因此在插入关键字(插入数据)时咱们须要按照以下规则插入:向当前结点中插入关键字后,判断当前结点的关键字数量是否小于等于 m-1,若是小于,则插入结束;不然须要将当前结点进行分裂,如何分裂呢?在 m/2 处拆分,造成左右两部分,即两个新的子结点,而后将 m/2 处的关键字移到父节点当中(从最中间分裂)。数据结构和算法

对于一个 5 阶的 B-Tree(也就是说,非根结点,关键字数量的范围为 2 <= k <= 4),咱们依次向树中插入以下数据:50,30,40,25,其流程以下。 首先依次插入 50、30、40、25,插入后结点状态以下; 源码分析

图1

而后当咱们再插入 20 时,当前结点中就存储了 5 个关键字,因为当前树是一颗 5 阶树,所以每一个结点最多只能存放 4 个关键字,所以此时就须要将当前结点分裂。怎么分裂呢?就是从最中间(m/2)处将结点分红左右两部分(5/2 向上取整是 3),所以从数字 30 所在的地方进行分裂,而后将数字 30 放入到父结点当中(因为此时父结点为空,所以新建一个结点,而后将 30 放入到该结点中),而后将 30 左边的两个数 20、25 构成一个新的结点,右边的两个数 40、50 构成一个新的节点,这两个新的结点分别指向关键字 30 的左右两边。示意图以下:学习

图2

继续向树中插入数据 1五、十、13。当插入到 13 时,咱们发现结点中的关键字的数量又超过了 m-1,所以又须要进行结点的分裂了。此时将中间的数 15 拆分出去,放到父结点当中,剩下的左右两部分分别构成新的结点。

图3

再继续向树中一次插入数据:1八、60、5五、4五、2六、1七、八、三、5。最后该 B-Tree 的结构以下图所示。

图4

文章中贴出的全是静态图,若是想体验 B-Tree 数据插入的动态过程,能够去下面这个学习网站中去手动插入数据,体验一下数据插入的动态过程。(网址:www.cs.usfca.edu/~galles/vis… 一个很是不错的学习数据结构与算法的网站)

2. 查找

B-Tree 的查找操做相对比较简单,和二叉查找树的查找相似。

  1. 先从根结点开始查找,依次遍历根结点的关键字,找到第一个不小于要查找数据的关键字;
  2. 判断要查找的数据是否等于当前关键字,若是等于则返回数据;
  3. 若是不等于,则表示要查找的数据是否小于当前关键字,所以进入当前关键字的左子树查找,查找过程和根结点的查找过程相似,重复上述步骤便可。

以上面 B-Tree 的数据为例,查找数字 10。首先从根结点开始查找,第一个不小于 10 的关键字是 20,因为要查找的数据10不等于关键字20,所以进入关键字 20 的左子树查找,此时指针指向关键字 八、15 所在的结点,在该结点中第一个不小于 10 的关键字是 15,因此进入关键字 15 的左子树查找,此时指针指向关键字 十、13 所在的结点,发现该结点中关键字 10 等于要查找的数据,所以返回。(以下图所示,红色表示查找过程当中的路径)

图5

若是要数字 60,过程和上面同样,先从根结点开始,发现根结点中全部的关键字都小于 60,因此进入根结点最后一个关键字的右子树查找,即进入关键字 20 的右子树查找。一样,发现关键字 30、50 所在的结点中,全部的关键字都小于 60,所以进入当前节点最后一个关键字 50 的右子树查找,最终查找到 60,返回数据。以下图所示,红色表示查找过程当中的路径)

图6

3. 删除

B-Tree 在数据的插入过程当中,为了知足 B-Tree 的性质,所以中间会出现结点的分裂过程,一样,在数据的删除过程当中,有可能由于删除了某个关键字而致使不知足 B-Tree 的相关性质了,所以在删除过程当中会出现结点的合并等状况,删除过程相对比较复杂,但整体来讲,能够归结为如下三种场景。

  1. 若是是叶子结点,删除关键字后,叶子结点中关键字的数量很多于 m/2 个,那么直接删除关键字便可;
  2. 若是是叶子结点,删除关键字后,叶子结点中关键字的数量少于 m/2 个,这个时候就不知足 B-Tree 的性质了,所以须要向兄弟结点借关键字。若是兄弟结点中关键字个数大于 m/2,那么就能够借,先将父节点移到当前节点中,而后兄弟结点的一个关键字移到父结点中;若是兄弟结点的关键字数量个数小于等于 m/2,假设兄弟结点借出一个关键字后,那么它本身的关键字数量就少于 m/2 了,又不符合 B-Tree 的性质了,所以这个时候不能借,此时须要将要删除的关键字删除后,将父节点移到此处,而后将当前节点和兄弟结点合并。
  3. 若是是非叶子节点删除关键字,那么就须要先删除当前关键字,而后用右子树中最小的关键字补上当前位置,再从右子树中删除刚刚补充上去的关键字,这个删除操做就又是B-Tree的删除操做了。(右子树中最小的关键字必定是在叶子结点中,因此删除过程就是删除叶子结点中的关键字了,也就是场景 1 和场景 2 的流程了)。

下面结合具体示例,针对上面 3 个场景分别举例来讲明删除操做的流程,如下面的数据为例。

图7

从 B-Tree 中删除关键字 29,因为关键字 29 所在的节点是叶子结点,当将 29 删除后,当前结点的关键字数量为 3,也就是说删除剩下的关键字数量很多于 m/2(5/2=2),知足上面提到的场景 1,那么就能够将关键字直接删除。

图8

继续从 B-Tree 中删除关键字 55,因为关键字 55 所在的节点是叶子结点,当将 55 删除后,当前结点剩下的关键字数量为 1 了,小于 m/2,所以须要向兄弟结点借关键字。当前结点的兄弟结点中有 4 个关键字(40、4五、4七、49),大于 m/2,因此能够借出关键字,符合场景 2 的第一种状况。所以先将关键字 55 删除,而后将父节点中关键字 50 移动到当前结点,再将兄弟结点中的关键字 49 移动到父结点中,示意图以下。

图9

继续从 B-Tree 中删除关键字 17,因为关键字 17 所在的节点是叶子结点,当将 17 删除后,当前结点剩下的关键字数量为 1 了,小于 m/2,所以须要向兄弟结点借关键字。当前结点的兄弟结点中有 2 个关键字(十、13),小于 m/2,因此不能够借出关键字,符合场景 2 的第二种状况。所以先将关键字 17 删除,而后将父节点中关键字 15 移动到当前结点,而后将当前结点与兄弟结点合并(关键字 十、13 所在的结点)。示意图以下。

图10

而后咱们发现,在关键字 17 删除的时候,咱们从父结点(关键字 8 所在的结点)中移下来一个关键字,它的父结点只剩下一个关键字了,父结点又不符合 B-Tree 的性质了,因此咱们还要继续操做。让父结点找本身的兄弟结点继续借关键字,父结点此时左边没有兄弟结点,所以找右边的兄弟结点(关键字 30 和 49 所在的结点)借关键字。结果发现右边兄弟结点的关键字个数也不大于 m/2,若是兄弟结点借出关键字后,又不符合性质了,因此这个时候又符合上面咱们提到的场景 2 的第二种状况,所以须要合并结点。因此接下来的操做是:将关键字 8 的父结点移到 8 所在的结点上,而后合并关键字 8 和 30、49 所在的结点。示意图以下:

图11

继续从 B-Tree 中删除关键字 20,此时 20 处于的结点是非叶子结点,所以知足场景 3。因此直接删除关键字 20,而后从 20 的右子树中取出最小的关键字 25 填充到 20 所在的位置,最后将 25 这个关键字从右子树的结点中删除,对于 25 这个关键字的删除流程,又能够分别对应上面叶子结点的删除场景了。示意图以下:

图12

以上就是 B-Tree 树中关键字的删除流程,相对于插入和查找过程,删除过程更加复杂,所以最好去这个可视化网站去学习下。(PC 端打开,网址:www.cs.usfca.edu/~galles/vis… 在这个可视化网站中,在删除非叶子结点的关键字的时候,取的是左子树中最大的关键字填充的,而本文讲解的时候说的是取右子树中最小的关键字填充,这二者其实本质上没有任何区别,都是为了知足 B-Tree 的性质,而且保证每一个结点上全部关键字是有序的。在学习数据结构和算法的过程当中,没有必要死抠细节,重要的是学习思惟。

总结

总结时刻。本文主要讲解了 B-Tree 相关的性质,结合示意图详细介绍了插入、查找、删除的过程。在文中的示例中,我特地没有往 B-Tree 中添加剧复的数据,那么若是往 B-Tree 中插入重复的数据后又应该怎么办呢?若是出现重复的数时,咱们只须要在插入的时候决定将重复的数放入到左子树中或者右子树中,这个具体放在哪边,能够本身定义。在查找数据的时候,就不能在找到一个符合要求的数据后就立马中止查找了,还须要继续日后查找,直到出现第一个不符合要求的数据才中止查找。一样,删除的时候,也须要删除全部的数据。

篇幅有限,所以B+Tree 和数据库索引相关的知识下一篇博客介绍。

相关

微信公众号
相关文章
相关标签/搜索