B树、B+树详解

B树、B+树详解

B树

前言

首先,为什么要总结B树、B+树的知识呢?最近在学习数据库索引调优相关知识,数据库系统普遍采用B树、B+树作为索引结构,例如 MYSQL的InnoDB引擎使用的就是B+树,理解不透彻B树,则无法理解数据库的索引机制,接下俩将用最简洁直白的内容来了解B树、B+树的数据结构。

另外,B-树,即为B树,因为B树的原英文名称为B-TREE,而国内很多人喜欢B-Tree译作B-树,其实,这是个很不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B-树又是一种树,而事实上,B-Tree就是指的是B树,目前B的意思理解为平衡。

B树的出现是为了弥合不同的存储级别之间的访问速度上的巨大差异,实现高效的I/O,平衡二叉树的查找效率是非常高的,这样会导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下,另外数据量过大会导致内存空间不够容纳平衡二叉树所有节点的情况,B树是解决这个问题的很好的结构。

概念

首先,B树不要和二叉树混淆,在计算机科学中,B树是一种自平衡数据结构,它维护有序数据并允许以对数时间进行搜索,顺序访问,插入和删除。B树是二叉搜索树的一般化,因为节点可以有两个以上的子节点,这与其他自平衡二进制搜索树不同,B树非常适合读取和写入相对较大的数据块(如光盘)的存储系统。它通常用于数据库和文件系统。

定义

B树是一种平衡的多分树,通常我们所说m阶的B树,它必须满足以下条件:

  • 每个节点最多只有m个子节点
  • 每个非叶子节点(除了根)具有至少【m/2】个子节点
  • 如果根不是叶子节点,则根至少有两个子节点
  • 具有k个子节点的非叶子节点包含k-1个键
  • 所有叶子节点都出现在同一水平,没有任何信息(高度一致)

什么是B树的阶?

B树中的一个节点的子节点数目的最大值,用m表示,假如最大值为10,则为10阶,如图:

W8aZSs.md.png

所有节点中,节点【13,16,19】拥有的子节点数目最多,四个子节点(灰色节点),所以可以定义为上面的树为4阶B树。

什么是根节点?

节点【10】即为根节点,特征:根节点拥有的子节点的数量的上限和内部节点相同,如果根节点不是树中唯一节点的话,至少有两个子节点(不然就变成单支了)。在m阶B树中(根节点非树中唯一节点),那么有关系式2<=M<=m,M为子节点数量;包含的元素数量1<=K<=m-1,K为元素数量。(其中,元素数量为每一个节点中的所包含的数的数量)。

什么是内部节点?

节点【13,16,19】、节点【3,6】都为内部节点,特征:内部节点是除叶子节点之外的所有节点,拥有父节点和子节点。假定m阶B树的内部节点的子节点的数量为M,则一定要符合(m/2)<=M<=m的关系式,包含元素数量M-1;包含的元素数(m/2)-1<=K<=m-1,K为元素数量。m/2向上取整。

什么是叶子节点?

节点【1,2】、节点【11,12】等最后一层都为叶子节点,叶子节点对元素数量有相同的限制,但是没有子节点,也没有指向子节点的指针。特征:在m阶B树种叶子节点的元素符合(m/2)-1<=K<=m-1

插入

针对m阶高度为h的B树,插入一个元素的时候,首先在B树中是否存在,如果不存在,即在叶子节点处结束,然后再叶子节点中插入该新的元素。

  • 若该节点元素个数小于m-1,直接插入。
  • 若该节点元素个数等于m-1,引起节点分裂;以该节点中间元素为分界,取中间元素(偶数个数,中间两个随机选取)插入到父节点中;
  • 重复上面的动作,直到所有节点符合B树的规则;最坏的情况一直分裂到根节点,生成新的根节点,高度增加1;

上面三段话为插入动作的核心,接下来以5阶B树为例,详细讲解插入的动作:

5阶B树的关键点:

  • 2<=根节点的子节点个数<=5
  • 3<=内节点子节点个数<=5
  • 1<=根节点元素个数<=4
  • 2<=内部节点元素个数<=4

W80Kmt.md.png

图(1)插入元素【8】后变为图(2),此时根节点的元素个数为5,不符合1<=根节点元素个数<=4,进行分裂,(真实情况是先分裂,然后插入元素,这里是为了直观而先插入元素,下面的操作都一样,不再赘述),取节点中间元素【7】,加入到父节点,左右分裂为两个节点,插入是在叶子节点处进行插入的,如图(3):

W8BMv9.md.png

接着插入元素【5】,【11】,【17】的时候,不需要任何分裂操作,如图【4】:

W8BBDI.md.png

插入元素【13】

W8BqGF.md.png

这时,节点数量超出最大数量,进行分裂,提取中间元素【13】,插入到父节点当中,如图6:

W8D1zQ.md.png

接着插入元素【6】,【12】,【20】,【23】的时候,不需要任何分裂操作,如图7:

W8riT0.md.png

插入26的时候,最右边的叶子节点满了,需要进行分裂操作,中间元素【20】上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的节点存在两个关键字元素。

W8rCmn.md.png

插入【4】的时候,导致最左边的叶子节点被分裂,【4】恰好也是中间元素,上移到父节点中,然后元素【16】,【18】,【24】,【25】陆续插入不需要任何分裂操作

W8rvAx.md.png

最后,当插入【19】的时候,含有【14】,【16】,【17】,【18】的节点需要分裂,把中间元素【17】上移到父节点中,但是情况来了,父节点中的空间已经满了,所以也要进行分裂,将父节点中的中间元素【13】上移到新形成的根节点中,这样具体插入操作的完成。

W8sAHI.md.png

删除

首先查找B树种需要删除的元素,如果该元素在B树种存在,则将该元素在其节点种进行删除;删除该元素后,首先判断该元素是否有左右孩子节点,如果有,则上移孩纸节点种的某相近元素(左孩纸最右边的节点或者右孩子最左边的节点)到父节点种,然后是移动之后的情况;如果没有,直接删除。

  • 某节点中元素数目小于(m/2)-1,(m/2)向上取整,则需要看其某相邻兄弟节点是否丰满
  • 如果丰满(节点中元素个数大于(m/2)-1),则向父节点借一个元素来满足条件
  • 如果其相邻兄弟的都不丰满,则其节点数目等于(m/2)-1,则该节点与其相邻的某一兄弟节点进行合并成一个节点。

接下来还以5阶B树为例子,详细讲解删除的动作:

关键要领:元素个数小于2(m/2-1)就合并,大于4(m-1)就分裂

如图依次删除【8】,【20】,【18】,【5】

W8Hdk8.md.png

首先删除元素【8】,当然先查找【8】,【8】在一个叶子节点中,删除后该叶子节点元素个数为2,符合B树规则,操作很简单,咱们只需要移动【11】至原来【8】的位置,移动【12】到【11】的位置(也就是节点中删除元素后面的元素向前移动)

W8bPBt.md.png

下一步,删除【20】,因为【20】没有在叶子节点中,而是在中间节点中找到,咱们发现他的继承者【23】(字母升序的下个元素),将【23】上移到【20】的位置,然后将孩子节点中的【23】进行删除,这里恰好删除后,该孩纸节点中元素个数大于2,无需进行合并操作。

W8bfbt.md.png

下一步删除【18】,【18】在叶子节点中,但是该节点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目为2,而由前面我们已经知道:如果其相邻某个兄弟节点中比较丰满,(元素个数大于ceil(5/2)-1=2),则可以向父节点借一个元素,然后将最丰满的相邻兄弟节点中上移最后或者最前一个元素到父节点中,在这个实例中,右相邻兄弟节点中比较丰满(3个元素大于2),所以先向父节点借一个元素【23】下移到该叶子节点中,代替原来【19】的位置,【19】前移,然后【24】在相邻兄弟节点中上移到父节点中,最后在相邻兄弟节点中删除【24】,后面元素前移。

W8LHns.md.png

最后一步删除【5】,删除后会导致很多问题,因为【5】所在的节点数目刚好全部达标,刚好满足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟节点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟节点进行合并操作;首先移动父节点中的元素(该元素再两个需要合并的两个节点元素之间)下移动到其子节点中,然后将1两个节点进行合并成一个节点。所以再改实例中,咱们首先将父节点的元素【4】下移动到已经删除【5】而只有【6】的节点中,然后将含有【4】和【6】的节点和含有【1】,【3】的相邻兄弟节点进行合并成一个节点。

W8Oxat.md.png
也许你认为这样删除操作已经结束了,其实不然,再看看上图,对于这种特殊情况,你会立即发现父节点只包含一个元素【7】,没达标(因为非根节点包括叶子节点的元素K必须满足于2<=K<=4,此处的K=1),这时不能够接受的。如果这个问题节点的相邻兄弟元素比较丰满,可以向父元素借一个元素。而此时兄弟节点元素刚好为2,刚刚满足,只能进行合并,而根节点中的唯一元素【13】下移到子节点,这样的高度减少一层。

W8jJXQ.md.png

磁盘I/O与预读

计算机存储设备一般分为两种:内存储器和外存储器器

内存储器为内存,内存存取速度快,但容量小,价格昂贵,而且不能长期保存数据(在不通电情况下数据会消失)。

外存储器即为磁盘读取,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。

考虑到磁盘I/O是非常高昂的操作,计算机操作系统做了一些优化,当一次I/O操作的时候,不光把当前磁盘地址的数据,而是把相邻的数据也读到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,其相邻的数据也很快被访问到。每一次IO读取的数据我们称之为第一页,具体第一页有多大的数据跟操作系统有关,一般为4k或者8k,也就是我们读取一页内容的时候,实际上才发生了一次IO操作,这个理论对于索引的数据结构设计非常有帮助。

事实1 : 不同容量的存储器,访问速度差异悬殊。

  • 磁盘(ms级别) << 内存(ns级别), 100000倍
  • 若内存访问需要1s,则一次外存访问需要一天
  • 为了避免1次外存访问,宁愿访问内存100次…所以将最常用的数据存储在最快的存储器中

事实2:从磁盘1中读取1B,与读写1KB的时间成本几乎一样。

从以上数据可以总结出一个道理,索引查询的数据主要受限于硬盘的IO速度,查询IO次数越少,速度越快,所以B树的结构才会应需求而生;B树的每个节点可以视为一次IO读取,树的高度表示最多的IO次数,在相同数量的总元素个数下,每个节点的元素个数越多,高度越低,查询所用的IO次数越少,假设,一次硬盘一次I/O数据为8K,索引用int(4字节)类型数据建立,理论上一个节点最多可以为2000个元素,200020002000=8000000000,80亿条的数据只需3次I/O(理论值),可想而知,B树做为索引的查询效率有多高;

另外也可以看出同样的总元素个数,查询效率和树的高度密切相关

B树的高度

一棵树含有N个总关键字树的m阶的B树的最大高度是多少?

log(m/2)(N+1)+1 log以(m/2)为底,(N+1)/2的对数再加1

算法如下:

WG99TH.md.png

B+树

  • 有m个子树的中间节点包含有m个元素(B树种是K-1个元素),每个元素不保存数据,只用来索引
  • 所有的叶子节点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子节点本身依依关键字的大小自小二大的顺序链接。(而B树的叶子节点并没有包含全部要查找的信息)。
  • 所有的非终端节点可以看成是索引部分,节点中仅含有其子树根节点中最大(或者最小)关键字。(而B树的非终端节点包含需要查找的有效信息);

为什么说B+树比B树更适合做数据库索引?

  1. B+树的磁盘读写代价更低

    B+树的内部节点并没有指向关键字具体信息的指针。因此其内部节点相对B树更小。如果把同一内部节点的关键字放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读取内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

  2. B+树查询效率更稳定

    由于非终端节点并不是最终指向文件内容的节点,而只是叶子节点关键字的索引。索引任何关键字的查找必须走一条根节点到叶子节点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. B+树便于范围查询(最重要的原因,范围查找是数据库的常态)

    B树再提高了IO性能的同时并没有解决掉元素遍历低下的问题,正是为了解决这个问题,B+树应用而出,B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且再数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

    不懂可以看看这篇解读-》范围查找

补充:B树的范围查找用的是中序遍历,而B+树用的是再链表上遍历
WGiZX6.md.png](https://imgtu.com/i/WGiZX6)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值