BTree BTree是一种多路搜索树,每个节点都包含一系列键和指针,最小的BTree节点包含4个键和5个指针。BTree中只有数据节点。
BTree是动态的。当添加或删除数据时,BTree的高度会随之增加或减少。
B+Tree B+Tree包含数据节点和索引节点。数据节点通常就是B+Tree的叶子节点(注:叶子节点指树最底层的节点,它没有子节点);而索引节点则通常是根节点或中间节点。
在添加或删除数据时,B+Tree会调整索引节点,因此,类似于BTree,B+Tree的高度也会动态地增加或减少。索引节点的内容和数量可以反映出这种变化。
B+Tree和BTree使用“填充系数(fill factor)”来判断节点是否需要重组,在所有B+Tree和BTree中,最小的填充系数是50%。本文使用了最小的节点结构,范例中的B+Tree符合如下规范:
每节点含键数 | 4 |
每节点含指针数 | 5 |
填充系数 | 50% |
每节点最少含键数 | 2 |
上表展示了每节点至少需要包含两个键,根节点可以除外。
下图是一个B+Tree,它的索引节点还未填满(根节点含有一个未使用的键空间)。另外,在第二个叶子节点上也有未使用的空间。
![]() |
向B+Tree添加数据
键的大小决定了数据的保存位置,叶子节点按键的大小排序,相邻两节点上保存着一对互相指向对方的指针。这种指针可以提高增删数据时节点重组的效率。
当我们向B+Tree添加数据时会面临三种情况,每种情况都会导致不同的插入算法,这三种情况为:
叶子节点已满 | 索引节点已满 | 行为 |
---|---|---|
否 | 否 | 在相应叶子节点插入数据并排序 |
是 | 否 | 1. 相应叶子节点拆分为左节点和右节点 2. 中间键复制到索引节点并排序 3. 键<中间键的数据放到左节点 4. 键>=中间键的数据放到右节点 |
是 | 是 | 1. 相应叶子节点拆分为左节点和右节点 2. 键<中间键的数据放到左节点 3. 键>=中间键的数据放到右节点 4. 相应索引节点拆分为左节点和右节点 5. 键<中间键的索引放到左节点 6. 键>中间键的索引放到右节点 7. 中间键移动到上一级索引节点 如果上一级索引节点也满了,继续拆分之 |
插入算法示例
下面用实例来解释上面三种情况。我们从最简单的情况开始:向未满的叶子节点插入数据。由于只在第二个叶子节点上有未使用空间,该叶子节点包含25和30,现在我们插入数据28。下图展示了插入结果。
![]() |
当叶子节点已满而索引节点未满时
现在,我们插入数据70。70被分配给第三个节点,包含50、55、60和65。不幸的是该节点已满,于是它按照如下方式被拆分:
左节点 | 右节点 |
---|---|
50, 55 | 60, 65, 70 |
中间键为60,它将被复制到索引结点并排列在50和75之间。
下图展示了插入70之后的结果。
![]() |
当叶子节点和索引节点都满时
最后我们插入数据95。95被分配给最后一个节点 ,包含75、80、85和90。由于该节点已满,它将被拆分为两个节点:
左节点 | 右节点 |
---|---|
75, 80 | 85, 90, 95 |
中间键为85,将被复制到索引节点。不幸的是,索引节点也满了,所以它也会被拆分:
左节点 | 中间键 | 右节点 |
---|---|---|
25, 50 | 60 | 75, 85 |
中间键为60,将被移动到上一层索引页 。下图展示了插入95后的结果:
![]() |
节点重组
B+Tree会通过节点重组来降低节点拆分率。当某个叶子节点已满,而与它相邻的节点未满时,便会发生重组。此时该叶子节点不会被拆分,而是将一条数据移动到相邻的叶子节点上,必要时会调整索引节点。通常情况下,左侧的节点(如果存在)会先于右侧的节点被选中。
举个例子,让我们回到向B+Tree插入70之前。前面已经说过70会被插入到包含50、55、60、65的叶子节点。注意该节点已满,但它的左侧节点尚有空间可用。
![]() |
通过节点重组我们将最小的键50移动到左侧节点 。由于50同时出现在索引节点中,所以索引节点也将被调整。重组结果如下图:
![]() |
从B+Tree中删除数据
从B+Tree中删除数据时也会面临三种情况,每一种情况导致不同的删除算法,这些情况是:
叶子节点饱和度<填充系数 | 索引节点饱和度<填充系数 | 行为 |
---|---|---|
否 | 否 | 从叶子节点中删除数据并重新排列数据。如果被删除数据的键同时出现在索引节点中,使用下一个相邻的键顶上。 |
是 | 否 | 将叶子节点和与它相邻的节点合并。同时修改索引节点中的相关键。 |
是 | 是 | 1. 将叶子节点和与它相邻的节点合并。 2. 修改索引节点中的相关键。 3. 将索引节点和与它相邻的节点合并。 如此反复,直到当前节点的饱和度>=填充系数或者到达根节点 |
现在让我们回到向B+Tree插入95之后,回顾一下,当时的B+Tree如下图所示:
![]() |
从B+Tree中删除70
我们先从B+Tree中删除70。70在第四个叶子节点,包含60、65和70。删除70之后,这个节点上还有2条数据,其饱和度为50%=填充系数,我们只需简单地删除70即可。下图展示了删除后的状态:
![]() |
从B+Tree中删除25
接着,我们从B+Tree中删除25。25在第二个叶子节点,包含25、28和30。删除25后该节点的饱和度为50%。同时,25出现在索引节点中,因此,25被删除后28需要顶上。
下图展示了删除后的状态:
![]() |
从B+Tree中删除60
最后,我们从B+Tree中删除60。这次删除就没那么简单了,原因如下:
1. 包含60和65的叶子节点在60被删之后,饱和度低于填充系数。所以需要合并相邻叶子节点 。
2. 叶子节点合并之后,上一级索引节点会删除一个键,其饱和度也会低于填充系数。所以需要合并相邻索引节点。
3. 60作为根节点中的唯一键,在删除之后,根节点也随之消失。
下图展示了删除60之后的B+Tree。注意只剩余一个索引节点。
![]() |