一. B树的定义

1.1. B树概念与使用场景

B树(B-tree,所以很多人又称为B-树)
是一种自平衡的树,一个节点可以拥有2个以上的子节点,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。

与自平衡二叉查找树不同,B树的每个节点可以包含大量的关键字信息和分支,便于降低自己的高度,让自己更胖更矮,更加适用于读写相对大的数据块的存储系统,例如磁盘,可以减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。
B树、B+树其实很简单,看不懂你找我_子节点

在B树中,内部(非叶子)节点可以拥有可变数量的子节点(数量范围预先定义好)。当数据被插入或从一个节点中移除,它的子节点数量发生变化。为了维持在预先设定的数量范围内,内部节点可能会被合并或者分离。因为子节点数量有一定的允许范围,所以B树不需要像其他自平衡查找树那样频繁地重新保持平衡,在这点名批评红黑树,但是由于节点没有被完全填充,可能浪费了一些空间。

B树的优势如下:

  • 使关键字保持排序顺序以进行顺序遍历
  • 使用分层索引来最大程度地减少磁盘读取次数
  • 使用部分完整的块来加快插入和删除
  • 使用递归算法使索引保持平衡

总之,B树操作不是很复杂,用途很广泛,正道的光,照在了数据结构的路上
B树、B+树其实很简单,看不懂你找我_数据库_02

1.2. B树定义

B树、B+树其实很简单,看不懂你找我_树结构_03
根据 Knuth 的定义,一个 m 阶的B树是一个有以下属性的树:

  • 每一个节点最多有 m 个子节点
  • 每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ (向上取整)个子节点
  • 如果根节点不是叶子节点,那么它至少有两个子节点
  • 有 k 个子节点的非叶子节点拥有 k − 1 个键
  • 所有的叶子节点都在同一层,叶子节点不包含任何关键字信息

但是其实文献中B树的术语并不统一
欧美在术语统一这方面一直都可以的 : )

术语的定义不一致。

  • Bayer & McCreightComer等人将B树的定义为非根节点拥有键的最小数量。Folk & Zoellick指出这一术语是模糊不清的。一个 3 阶B树键的最大数量可能为 6 或 7。 Knuth 通过将阶定义为最大数量的子节点(键值数的最大值+1)来避免了这一问题。

术语叶子的定义也不一致。

  • Bayer & McCreight认为叶子层是最下面一层的键,此时这些叶子节点只包含键值,子节点指针都指向不含任何信息的空的记录。
  • Knuth 认为叶子层是最下面一层键之下的一层。如同红黑树那般(红黑树将Null指针作为黑色叶子节点),将Null作为叶子节点,此时叶子节点不存储键值等任何信息。
  • 我们一般以按Knuth大佬的说法来,清华的严蔚敏老师的数据结构一书中也是这么定义的。

内部节点:

  • 每个内部节点都至少含有 ⌈m/2⌉ 个子节点,所以至少都是半满
  • 当父节点还没满时,一个满子节点可以被分为两个合法子节点
  • 每个内部节点(除叶子节点和根节点之外的所有节点)包含以下信息
  • 指向父节点的指针​​Parent*​
  • 键值的个数​​KeyNum​
  • 键值​​Key​
  • KeyNum+1个指向子节点的指针​​Ptr*​

根节点:

  • 根节点的子节点数的最大值和内部节点一样都是m
  • 根节点的子节点数没有限制,当根节点储存的键值数小于⌈m/2⌉时,根节点可以没有子节点,此时根节点为叶子节点。

B树、B+树其实很简单,看不懂你找我_数据库_04

二. B树的操作

在B树中,内部(非叶子)节点可以在某个预定义范围内具有可变数量的子节点。 从节点插入或删除数据时,其子节点数会更改。 为了维持预定范围,可以将内部节点连接或拆分。

2.1. m阶B树的高度

对硬盘访问的IO次数取决于B树的高度,B树的高度如何确定呢?

首先明确的是不带任何信息的一层叶节点不算做B树的高度,通常,设一个m阶B树非叶子节点内部的关键字范围在d~2d范围内,d = ⌈m/2⌉

B树、B+树其实很简单,看不懂你找我_树结构_05

B树、B+树其实很简单,看不懂你找我_数据库_06
B树、B+树其实很简单,看不懂你找我_算法_07
B树、B+树其实很简单,看不懂你找我_数据库_08
B树、B+树其实很简单,看不懂你找我_二叉树_09
B树、B+树其实很简单,看不懂你找我_子节点_10
B树、B+树其实很简单,看不懂你找我_二叉树_11
B树、B+树其实很简单,看不懂你找我_数据库_12
B树、B+树其实很简单,看不懂你找我_算法_13

B树、B+树其实很简单,看不懂你找我_算法_14
B树、B+树其实很简单,看不懂你找我_树结构_15
B树、B+树其实很简单,看不懂你找我_树结构_16
B树、B+树其实很简单,看不懂你找我_算法_17
B树、B+树其实很简单,看不懂你找我_树结构_18
B树、B+树其实很简单,看不懂你找我_二叉树_11
B树、B+树其实很简单,看不懂你找我_数据库_20
B树、B+树其实很简单,看不懂你找我_子节点_21

看上去很复杂,其实就是等差数列求和
B树、B+树其实很简单,看不懂你找我_子节点_22

2.2.插入

所有的插入都从根节点开始。要插入一个新的元素,首先搜索这棵树找到新元素应该被添加到的对应节点。将新元素插入到这一节点中的步骤如下:

  • 如果节点拥有的元素数量小于最大值,那么有空间容纳新的元素。将新元素插入到这一节点,且保持节点中元素有序。
  • 否则的话这一节点已经满了,将它平均地分裂成两个节点:
  • 从该节点的原有元素和新的元素中选择出中位数,小于这一中位数的元素放入左边节点,大于这一中位数的元素放入右边节点,中位数作为分隔值。
  • 分隔值被插入到父节点中,这可能会造成父节点分裂,分裂父节点时可能又会使它的父节点分裂,以此类推。如果没有父节点(这一节点是根节点),就创建一个新的根节点(增加了树的高度)。如果分裂一直上升到根节点,那么一个新的根节点会被创建,它有一个分隔值和两个子节点。这就是根节点并不像内部节点一样有最少子节点数量限制的原因。

例子,已有4阶B树如下
B树、B+树其实很简单,看不懂你找我_数据库_23
插入4,根据分层查找关键字,直接插入:
B树、B+树其实很简单,看不懂你找我_子节点_24
插入17,根据分层查找关键字,直接插入:
B树、B+树其实很简单,看不懂你找我_数据库_25
插入20,子节点中关键字达到m-1,故无法继续插入,子节点分割,16分至父节点:
B树、B+树其实很简单,看不懂你找我_树结构_26
插入21、22,直到分裂出新的根节点
B树、B+树其实很简单,看不懂你找我_数据库_27

2.3.删除

删除叶子节点中的元素

  • 搜索要删除的元素
  • 如果它在叶子节点,将它从中删除
  • 如果发生了下溢出,按照后面 “删除后重新平衡”部分的描述重新调整树

删除内部节点中的元素
内部节点中的每一个元素都作为分隔两颗子树的分隔值,因此我们需要重新划分。值得注意的是左子树中最大的元素仍然小于分隔值,右子树中最小的元素仍然大于分隔值,这两个元素都在叶子节点中,并且任何一个都可以作为两颗子树的新分隔值。算法的描述如下:

  • 选择一个新的分隔符(左子树中最大的元素或右子树中最小的元素),将它从子节点中移除,替换掉被删除的元素作为新的分隔值。
  • 前一步删除了一个子节点中的元素。如果这个子节点拥有的元素数量小于最低要求,那么从这一子节点开始重新进行平衡。

删除后的重新平衡
重新平衡从叶子节点开始向根节点进行,直到树重新平衡。如果删除节点中的一个元素使该节点的元素数量低于最小值,那么一些元素必须被重新分配。通常,移动一个元素数量大于最小值的兄弟节点中的元素。如果兄弟节点都没有多余的元素,那么缺少元素的节点就必须要和他的兄弟节点合并。合并可能导致父节点失去了分隔值,所以父节点可能缺少元素并需要重新平衡。合并和重新平衡可能一直进行到根节点,根节点变成惟一缺少元素的节点。重新平衡树的递归算法如下:

  • 如果缺少元素节点的右兄弟存在且拥有多余的元素,那么向左旋转,这里的左旋转与AVL中的左旋转类似。
  • 将父节点的分隔值复制到缺少元素节点的最后(分隔值被移下来;缺少元素的节点现在有最小数量的元素)
  • 将父节点的分隔值替换为右兄弟的第一个元素(右兄弟失去了一个节点但仍然拥有最小数量的元素)
  • 树又重新平衡
  • 否则,如果缺少元素节点的左兄弟存在且拥有多余的元素,那么向右旋转
  • 将父节点的分隔值复制到缺少元素节点的第一个节点(分隔值被移下来;缺少元素的节点现在有最小数量的元素)
  • 将父节点的分隔值替换为左兄弟的最后一个元素(左兄弟失去了一个节点但仍然拥有最小数量的元素)
  • 树又重新平衡
  • 否则,如果它的两个直接兄弟节点都只有最小数量的元素,那么将它与一个直接兄弟节点以及父节点中它们的分隔值合并
  • 将分隔值复制到左边的节点(左边的节点可以是缺少元素的节点或者拥有最小数量元素的兄弟节点)
  • 将右边节点中所有的元素移动到左边节点(左边节点现在拥有最大数量的元素,右边节点为空)
  • 将父节点中的分隔值和空的右子树移除(父节点失去了一个元素)
  • 如果父节点是根节点并且没有元素了,那么释放它并且让合并之后的节点成为新的根节点(树的深度减小)
  • 否则,如果父节点的元素数量小于最小值,重新平衡父节点

先有4阶B树:
B树、B+树其实很简单,看不懂你找我_树结构_28
删除22,分层查询键值后,在叶子节点中直接删除:
B树、B+树其实很简单,看不懂你找我_算法_29
删除3,父节点从右儿子那选择最小值(或左儿子那选择最大值),为新的键值(分隔值):
B树、B+树其实很简单,看不懂你找我_二叉树_30
删除5,此时左兄弟只有一个键值1,没有多余键值,借不了,所以需要合并,将父节点中的分隔值4加入到左儿子节点,右儿子剩下的所有元素合并到左儿子中,合并后父节点为键值数为0,小于规定的最小键值数1(⌈m/2⌉-1),对父节点递归算法

在新一轮的算法中,对刚才失衡的父节点进行左旋转:
B树、B+树其实很简单,看不懂你找我_子节点_31

三. B+树的定义

3.1.B+树的定义

B+树可以视为B树的一个变种,不同的是B+树内部节点不保存数据,非叶结点中的每个引索项只含有对应子树的最大关键字和指向子树的指针,不含有该关键字对应记录的存储地址,例如关系型数据库Mysql。
一颗m阶的B+树需要满足下列条件:

  • 每个分支结点最多包含m棵子树
  • 非根结点至少有两棵子树,其他每个分支结点至少含有B树、B+树其实很简单,看不懂你找我_二叉树_32棵子树
  • 如果根节点不是叶子节点,那么它至少有两个子节点
  • 结点的子树个数与关键字个数相同,所有分支结点中仅包含它的子结点中关键字最大值以及指向该子结点的指针
  • 所有叶结点包含全部关键字以及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
    B树、B+树其实很简单,看不懂你找我_二叉树_33

B树、B+树其实很简单,看不懂你找我_二叉树_34

3.2.B+树 VS B树

B+树相对于B树的优势有:

  • 内部节点不保存数据,占用空间小,所以每个节点可以存储的索引更多,即每个磁盘页存储的索引更多,树的高度更低,IO次数更低,磁盘查询效率更高
  • 每次查询都要到达最下面的叶子层才能拿到数据,性能更加稳定
  • 所有数据都保存在叶子节点,所有叶子节点都顺次连接,有利于遍历操作

现在你知道为什么数据库用B+树而不用B树了吧。

四. B+树的操作

4.1.插入

如果节点超过节点中键值数的规定范围则处于违规状态

  • 首先,查找要插入其中的节点的位置。接着把值插入这个节点中。
  • 如果没有节点处于违规状态则处理结束。
  • 如果某个节点有过多元素,则把它分裂为两个节点,每个都有最小数目的元素,同时将分隔值复制到父节点中作为索引。
  • 在树上递归向上继续这个处理直到到达根节点,如果根节点被分裂,则创建一个新根节点。

现有一个4阶B+树
B树、B+树其实很简单,看不懂你找我_算法_35
插入2,分层遍历索引,直接插入:
B树、B+树其实很简单,看不懂你找我_算法_36
插入8,分层遍历索引,节点已满,于是分裂,将新的间隔值6复制到父节点中作为索引,产生两个新的子节点:
B树、B+树其实很简单,看不懂你找我_子节点_37

4.2.删除删除

  • 首先,在叶子节点中查找要删除的值。接着从包含它的节点中删除这个值。
  • 如果没有节点处于违规状态则处理结束。
  • 如果节点处于违规状态则有两种可能情况:
  • 它的兄弟节点,就是同一个父节点的子节点,可以把一个或多个它的子节点转移到当前节点,而把它返回为合法状态,最后再使用从兄弟那借到的键值替代父节点中的分隔键值之后处理结束。
  • 它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中,并删除父节点中的分隔键值
  • 如果删除后的父节点键值数量不足,进行与B树完全相同的删除平衡操作

现有一4阶B+树:
B树、B+树其实很简单,看不懂你找我_子节点_37
删除8,分层遍历索引,直接删除:
B树、B+树其实很简单,看不懂你找我_二叉树_39
删除2,找左兄弟借一个数据2,用借来的键值2替代父节点中初始的索引2:
B树、B+树其实很简单,看不懂你找我_算法_40
删除4,兄弟节点没有数据可以借,于是与左兄弟节点合并,删除父节点的索引4:
B树、B+树其实很简单,看不懂你找我_子节点_41
删除5、6,删除5时可以向右兄弟借到6,删除6时就完全借不到了,只能合并,再删除父节点中的索引
B树、B+树其实很简单,看不懂你找我_数据库_42
删除7,兄弟节点借不到,合并后删除父节点中的索引,父节点不符合规则,对父节点进行删除后的平衡操作,父亲节点的兄弟节点只有一个键值2,于是父节点与兄弟节点和父节点的索引3进行合并
B树、B+树其实很简单,看不懂你找我_二叉树_43
B树、B+树其实很简单,看不懂你找我_树结构_44

红黑树传递门​,冲冲冲
B树、B+树其实很简单,看不懂你找我_树结构_45