B树(B-tree,所以很多人又称为B-树)
是一种自平衡的树,一个节点可以拥有2个以上的子节点,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。
与自平衡二叉查找树不同,B树的每个节点可以包含大量的关键字信息和分支,便于降低自己的高度,让自己更胖更矮,更加适用于读写相对大的数据块的存储系统,例如磁盘,可以减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。
在B树中,内部(非叶子)节点可以拥有可变数量的子节点(数量范围预先定义好)。当数据被插入或从一个节点中移除,它的子节点数量发生变化。为了维持在预先设定的数量范围内,内部节点可能会被合并或者分离。因为子节点数量有一定的允许范围,所以B树不需要像其他自平衡查找树那样频繁地重新保持平衡,在这点名批评红黑树,但是由于节点没有被完全填充,可能浪费了一些空间。
B树的优势如下:
总之,B树操作不是很复杂,用途很广泛,正道的光,照在了数据结构的路上
根据 Knuth 的定义,一个 m 阶的B树是一个有以下属性的树:
但是其实文献中B树的术语并不统一
欧美在术语统一这方面一直都可以的 : )
术语阶的定义不一致。
术语叶子的定义也不一致。
内部节点:
Parent*
KeyNum
Key
Ptr*
根节点:
在B树中,内部(非叶子)节点可以在某个预定义范围内具有可变数量的子节点。 从节点插入或删除数据时,其子节点数会更改。 为了维持预定范围,可以将内部节点连接或拆分。
对硬盘访问的IO次数取决于B树的高度,B树的高度如何确定呢?
通常,设一个m阶B树非叶子节点内部的关键字范围在d~2d范围内,d = ⌈m/2⌉
设 h 为 树 的 高 度 ( h ≥ − 1 ) , 空 树 的 高 度 表 示 为 − 1 n 为 树 中 键 值 的 总 个 数 ( n ≥ 0 ) , 空 树 的 总 键 值 数 为 0 设h为树的高度(h\ge-1),空树的高度表示为-1\\n为树中键值的总个数(n\ge0),空树的总键值数为0 设h为树的高度(h≥−1),空树的高度表示为−1n为树中键值的总个数(n≥0),空树的总键值数为0
当 内 部 关 键 字 为 2 d 即 m 时 , 树 的 高 度 有 最 小 值 当内部关键字为2d即m时,树的高度有最小值 当内部关键字为2d即m时,树的高度有最小值
h = 0 时 , 有 n 0 = m − 1 h=0时,有n_0 = m-1 h=0时,有n0=m−1
h = 1 时 , 有 n 1 = m ( m − 1 ) + n 0 = ( m + 1 ) ( m − 1 ) = m 2 − 1 h=1时,有n_1=m(m-1)+n_0=(m+1)(m-1)=m^2-1 h=1时,有n1=m(m−1)+n0=(m+1)(m−1)=m2−1
h = 2 时 , 有 n 2 = m 2 ( m − 1 ) + n 1 = ( m 2 + m + 1 ) ( m − 1 ) = m 3 − 1 h=2时,有n_2=m^2(m-1)+n_1=(m^2+m+1)(m-1)=m^3-1 h=2时,有n2=m2(m−1)+n1=(m2+m+1)(m−1)=m3−1
… \dots …
h = h 时 , 有 n = m h + 1 − 1 h=h时,有n=m^{h+1}-1 h=h时,有n=mh+1−1
所 以 h m i n = l o g m ( n + 1 ) − 1 所以h_{min}=log_m(n+1)-1 所以hmin=logm(n+1)−1
当 内 部 关 键 字 为 d 时 即 ⌈ m / 2 ⌉ 时 , 树 的 高 度 有 最 大 值 当内部关键字为d时即{\lceil m/2\rceil}时,树的高度有最大值 当内部关键字为d时即⌈m/2⌉时,树的高度有最大值
h = 0 时 , 非 叶 子 根 节 点 最 少 两 个 子 节 点 , 有 n 0 = 1 h=0时,非叶子根节点最少两个子节点,有n_0=1 h=0时,非叶子根节点最少两个子节点,有n0=1
h = 1 时 , 有 n 1 = 2 ( ⌈ m / 2 ⌉ − 1 ) + 1 = 2 ⌈ m / 2 ⌉ − 1 h=1时,有n_1=2({\lceil m/2\rceil}-1) + 1=2{\lceil m/2\rceil}-1 h=1时,有n1=2(⌈m/2⌉−1)+1=2⌈m/2⌉−1
h = 2 时 , 有 n 2 = 2 ( ⌈ m / 2 ⌉ − 1 ) ⌈ m / 2 ⌉ + 1 + n 1 = 2 ⌈ m / 2 ⌉ 2 − 1 h=2时,有n_2=2({\lceil m/2\rceil}-1){\lceil m/2\rceil} + 1+n_1=2{\lceil m/2\rceil}^2-1 h=2时,有n2=2(⌈m/2⌉−1)⌈m/2⌉+1+n1=2⌈m/2⌉2−1
… \dots …
n = 2 ⌈ m / 2 ⌉ h − 1 n=2{\lceil m/2\rceil}^{h}-1 n=2⌈m/2⌉h−1
h m a x = l o g ⌈ m / 2 ⌉ ( n + 1 2 ) h_{max}=log_{\lceil m/2\rceil}{(\frac{n+1}2)} hmax=log⌈m/2⌉(2n+1)
所有的插入都从根节点开始。要插入一个新的元素,首先搜索这棵树找到新元素应该被添加到的对应节点。将新元素插入到这一节点中的步骤如下:
例子,已有4阶B树如下
插入4,根据分层查找关键字,直接插入:
插入17,根据分层查找关键字,直接插入:
插入20,子节点中关键字达到m-1,故无法继续插入,子节点分割,16分至父节点:
插入21、22,直到分裂出新的根节点
删除叶子节点中的元素
删除内部节点中的元素
内部节点中的每一个元素都作为分隔两颗子树的分隔值,因此我们需要重新划分。值得注意的是左子树中最大的元素仍然小于分隔值,右子树中最小的元素仍然大于分隔值,这两个元素都在叶子节点中,并且任何一个都可以作为两颗子树的新分隔值。算法的描述如下:
删除后的重新平衡
重新平衡从叶子节点开始向根节点进行,直到树重新平衡。如果删除节点中的一个元素使该节点的元素数量低于最小值,那么一些元素必须被重新分配。通常,移动一个元素数量大于最小值的兄弟节点中的元素。如果兄弟节点都没有多余的元素,那么缺少元素的节点就必须要和他的兄弟节点合并。合并可能导致父节点失去了分隔值,所以父节点可能缺少元素并需要重新平衡。合并和重新平衡可能一直进行到根节点,根节点变成惟一缺少元素的节点。重新平衡树的递归算法如下:
先有4阶B树:
删除22,分层查询键值后,在叶子节点中直接删除:
删除3,父节点从右儿子那选择最小值(或左儿子那选择最大值),为新的键值(分隔值):
删除5,此时左兄弟只有一个键值1,没有多余键值,借不了,所以需要合并,将父节点中的分隔值4加入到左儿子节点,右儿子剩下的所有元素合并到左儿子中,合并后父节点为键值数为0,小于规定的最小键值数1(⌈m/2⌉-1),对父节点递归算法
B+树可以视为B树的一个变种,不同的是B+树内部节点不保存数据,只保存索引,所有的数据都保存在叶子节点中,每个叶子节点都保存有相邻叶子节点的指针,各叶子节点自小到大顺次连接,其余的地方与B树基本相同。B+树被广泛应用在数据库索引中,例如关系型数据库Mysql。
关于每个内部节点最大键值数与其最大子节点数目是否一致问题是存在争议的,有些人认为二者应该相同,另一部分人认为B+树应该和B树一样,最大键值数=最大子节点数-1,根据wiki百科加上我倾向于统一,剩下的内容基于第二种观点,与B树保持一致。
B+树相对于B树的优势有:
现在你知道为什么数据库用B+树而不用B树了吧。
如果节点超过节点中键值数的规定范围则处于违规状态
现有一个4阶B+树
插入2,分层遍历索引,直接插入:
插入8,分层遍历索引,节点已满,于是分裂,将新的间隔值6复制到父节点中作为索引,产生两个新的子节点:
现有一4阶B+树:
删除8,分层遍历索引,直接删除:
删除2,找左兄弟借一个数据2,用借来的键值2替代父节点中初始的索引2:
删除4,兄弟节点没有数据可以借,于是与左兄弟节点合并,删除父节点的索引4:
删除5、6,删除5时可以向右兄弟借到6,删除6时就完全借不到了,只能合并,再删除父节点中的索引
删除7,兄弟节点借不到,合并后删除父节点中的索引,父节点不符合规则,对父节点进行删除后的平衡操作,父亲节点的兄弟节点只有一个键值2,于是父节点与兄弟节点和父节点的索引3进行合并