一看就懂的B+树

本篇对于B+树讲的很细,希望大家要耐着性子看下去,保证会有收获!

相信大家都学过MySQL数据库,在开发中,为了加快数据库中数据的查找速度,一般会对表中的数据创建索引。MySQL数据库的索引就是用B+树来进行实现的。

实现一个需求:按值查找和按区间查找

如何通过索引来加快数据库表的查询速度呢?

#根据某个具体值来查找数据:select * from user where id = 1234;

#根据某个区间值来查询数据:select * from user where id > 1234 and id < 2345;

除了功能性需求外,在执行效率方面,我们希望通过索引让数据查询更快;在内存消耗方面,希望索引不用消耗太多的内存空间。

基于哈希表和二叉查找树的解决方案

对于支持快速查询操作的数据结构,我们学过了哈希表和平衡二叉查找树。

哈希表的查询性能很好,而且时间复杂度为O(1),但是它不支持按照区间进行快速查找,尽管平衡二叉查找树的性能也很好,时间复杂度为O(logn),而且使用中序遍历还可以输出有序的序列,但是也是不能满足按照区间快速查找的需求。索引B+树就应运而生!

基于B+树的解决方案

为了支持按照区间快速查找数据,我们对二叉查找树进行改造,使其具有按区间查找的功能,在原本二叉查找树下再加入一层节点,并且这层节点存储在有序链表中,将哈希表与有序链表进行结合使用。现在改造的二叉查找树就相当于将二叉查找树与有序链表结合使用。

如图所示:

 如果要查询某个区间的位置,那么只要将区间的初始值在数中进行查找,当定位到有序链表中的某个节点时,再从这个节点顺序的依次向后遍历,直到有序链表中的值大于区间的终止值为止。遍历有序链表得到的数据就是落在要查找区间范围内的数据。如图所示

此时又会出现一个问题,如果数据库中某个表的数据量很大,对应的索引也很大,将索引存储在内存中,占用的内存也就更多,这不是我们希望看到的。举个例子,给一亿个数据构建二叉查找树索引,那么所有就会包含大约2亿个节点,每个节点假设占用16B,就需要占用大约2GB大小的内存空间。也就是说,给一个表构建所有需要消耗2GB大小的内存,那么给10个表构建索引,就需要消耗20GB的内存,显然目前内存是不可能承受的数据。既然需要消耗那么多的内存,那是不是可以用时间来换取空间呢?

所以我们可以采用时间获取空间的方法,也就是把索引存在磁盘中,而不是存在内存中。大家都知道,磁盘是外存,访问的速度很慢,形象一点说,访问内存的速度是纳秒级别,那么访问磁盘的速度就是 毫秒级别的。所以,对于同样的时间,从磁盘中读取数据花费的时间要比内存满上几十倍。对于这种将索引存储到磁盘的方案,如果每读取一个节点都要对应一次磁盘IO操作,尽管内存消耗减少了,但是效率却是慢了好多好多,那么,还能不能继续优化呢?

为了避免重复的IO操作,导致的性能问题,我们需要尽可能地减少IO操作。前面说过,树上很多操作的时间复杂度跟树的高度成正比,降低树的高度,就可以减少磁盘IO操作。问题又来了,怎么降低二叉树的高度呢?
是不是这样,如果我们把索引构建成m叉树(m > 2),高度是不是比二叉树小呢?

我们对16个数据构建二叉树索引(改造后的二叉查找树),树的高度是4,查找一个数据需要4次磁盘IO操作(如果根节点存储在内存,其他节点存储在磁盘中)。如果我们对16个数据构建五叉树索引,那么树的高度为2,查找一个数据需要2次磁盘IO。如果我们对1亿个数据构建索引,那么我们需要构建100叉树,树的高度仅仅为4,查找一次数据仅仅需要4次磁盘IO操作。问题就解决啦!

事实上,m叉树与有序链表构建成的m叉树索引就是B+树。结构如下图。

 现在看看B+树对应的代码实现

/**
2 * 这是 B+ 树非叶子节点的定义。
3 *
4 * 假设 keywords=[3, 5, 8, 10]
5 * 4 个键值将数据分为 5 个区间:(-INF,3), [3,5), [5,8), [8,10), [10,INF)
6 * 5 个区间分别对应:children[0]...children[4]
7 *
8 * m 值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
9 * PAGE_SIZE = (m-1)*4[keywordss 大小]+m*8[children 大小]
10 */
11 public class BPlusTreeNode {
12    public static int m = 5; // 5 叉树
13    public int[] keywords = new int[m-1]; // 键值,用来划分数据区间
14    public BPlusTreeNode[] children = new BPlusTreeNode[m];// 保存子节点指针
15 }
16
17 /**
18 * 这是 B+ 树中叶子节点的定义。
19 *
20 * B+ 树中的叶子节点跟内部结点是不一样的,
21 * 叶子节点存储的是值,而非区间。
22 * 这个定义里,每个叶子节点存储 3 个数据行的键值及地址信息。
23 *
24 * k 值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
25 * PAGE_SIZE = k*4[keyw.. 大小]+k*8[dataAd.. 大小]+8[prev 大小]+8[next 大小]
26 */
27 public class BPlusTreeLeafNode {
28    public static int k = 3;
29    public int[] keywords = new int[k]; // 数据的键值
30    public long[] dataAddress = new long[k]; // 数据地址
31
32    public BPlusTreeLeafNode prev; // 这个结点在链表中的前驱结点
33    public BPlusTreeLeafNode next; // 这个结点在链表中的后继结点
34 }

 现在想想这个问题,是不是相同数据的情况下,m越大,树的高度越小,那么是不是m叉树中的m是不是越大越好呢?

无论是内存中的数据,还是磁盘中的数据,操作系统都是按页(1页大小通常是4KB)来读取的,一次读一页的数据。如果读取的数据量超过一页的大小,就会触发多次IO操作。这个时候我们应该就要想到,不能将读取的数据量超过一页的大小,因此,在选择m大小的时候,要尽量让每个节点的大小等于一页的大小。这样,读取一个节点就需要一次IO操作。尽管索引可以大大提高数据库的查询效率,但是,肯定会带来弊端,对于数据的写入过程会涉及索引的更新,因此,也会导致写入数据的效率下降。

对于一个B+树,m值是根据页的大小事先计算好的,也就是说每个节点最多只能有m个子节点,在往数据库写入数据时,有可能某些节点的子结点的个数大于m,这个时候,就需要再次进行调整。

这个时候我们可以将这个节点进行分裂,这个节点分裂成之后,其上层父节点的子结点个数就有可能超过m个(为什么会超过m继续往下看),然后父节点也可以按照这个方法进行分裂。这种联级反应会从上往下,一直影响到根节点。

因为要时刻保证B+树索引是一个m叉树,所以索引的存在会导致数据库写入的速度降低,同时删除的速度也可能降低,因为在删除数据的时候,也需要更新索引节点。频繁的数据删除操作会导致某些节点中子结点的个数变得非常少,如果时间久了,每个节点的子结点数目多了,会影响索引的效率。

针对这种情况,我们可以设置一个阀值,如m/2,如果某个节点的子结点个数小于m/2,就将它相邻节点的兄弟节点进行合并。如果合并之后的子结点个数超过m,仍然使用刚刚插入的方法进行分裂。

下面从一些图来详细进行了解。

B树插入

插入操作

对于插入操作很简单,只需要记住一个技巧即可:当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的

下面以一颗5阶B+树的插入过程为例,5阶B+树的节点最少2个元素,最多4个元素。

  • 插入5,10,15,20

  • 插入25,此时元素数量大于4个了,分裂

 

  • 接着插入26,30,继续分裂(中间元素的值分裂到父节点做元素的值)

 

 现在应该就知道父节点的子节点为什么会超过m个了。

下面看看删除的操作

删除操作

对于删除操作是比B树简单一些的,因为叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key,下面我们看看具体的实例。

  • 初始状态

  • 删除10,删除后,不满足要求,发现左边兄弟节点有多余的元素,所以去借元素,最后,修改父节点索引
  • 删除元素5,发现不满足要求,并且发现左右兄弟节点都没有多余的元素,所以,可以选择和兄弟节点合并,最后修改父节点索引
  • 发现父节点索引也不满足条件,所以,需要做跟上面一步一样的操作

 关于删除操作的一些细节我们补充一些关于B树的删除操作,帮助大家理解B+树的删除

B树的删除操作

B树的删除操作相对于插入操作是相对复杂一些的,但是,你知道记住几种情况,一样可以很轻松的掌握的。

  • 现在有一个初始状态是下面这样的B树,然后进行删除操作。
  • 删除15,这种情况是删除叶子节点的元素,如果删除之后,节点数还是大于m/2,这种情况只要直接删除即可。

  • 接着,我们把22删除,这种情况的规则:22是非叶子节点,对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。对于删除22,需要将后继元素24移到被删除的22所在的节点。

此时发现26所在的节点只有一个元素,小于2个(m/2),这个节点不符合要求,这时候的规则(向兄弟节点借元素):如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点。这样就满足要求了。

我们看看操作过程就更加明白了。

  • 接着删除28,删除叶子节点,删除后不满足要求,所以,我们需要考虑向兄弟节点借元素,但是,兄弟节点也没有多的节点(2个),借不了,怎么办呢?如果遇到这种情况,首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点

移动之后,跟兄弟节点合并。

B+树的特点

1.B+树由m叉查找树和有序链表组成

2.每个节点的个数不能超过m,也不能小于m/2

3.根节点的子节点可以不超过m/2,这是一个例外

4.一般情况下,根节点存储在内存,其他节点存储在磁盘

5.B+树中的节点不存储数据。

6.非根节点元素范围:m/2 <= k <= m-1

 现在关于B+树应该讲解的很详细了,大家都能理解的差不多了。

文章部分摘自:https://segmentfault.com/a/1190000020416577

参考书籍:《数据结构与算法之美》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MySQLB树B+树在数据结构上有一些区别。 B树是一种平衡多路搜索树,每个节点可以存储多个关键字和对应的指针。B树的节点数比B+树要多,因为B树的每个节点都存储了关键字和指针,而B+树的非叶子节点只存储了关键字,指针都放在叶子节点中。这意味着在同样大小的区域内,B树可以存储更少的关键字。 B+树是一种变体的B树,它也是一种平衡多路搜索树,但是只有叶子节点存储了关键字和对应的指针,而非叶子节点只存储了关键字。B+树的叶子节点通过指针连接在一起,形成一个有序链表, 这样可以方便进行区间查找和范围查询。B+树还具有更好的顺序访问性能和更高的磁盘利用率。 因此,B树适合在内存中进行操作,而B+树则更适合在磁盘上进行存储和查询操作。B+树数据库索引中常用于提高查询效率和范围查询的性能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [一文彻底搞MySQL基础:B树B+树的区别](https://blog.csdn.net/m0_54864585/article/details/125383198)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值