Skip to content

为了实现动态多层索引,通常采用 B-树 和 B+树。但是,用于索引的 B-树 存在缺陷,它的所有中间结点均存储的是数据指针(指向包含键值的磁盘文件块的指针),与该键值一起存储在B-树的结点中。这就会导致可以存储在 B-树中的结点数极大地减少了,从而增加 B-树的层数,进而增加了记录的搜索时间。

B+树通过仅在树的叶子结点中存储数据指针而消除了上述缺陷。因此,B+树的叶结点的结构与 B-树的内部结点的结构完全不同。在这里应该注意,由于数据指针仅存在于叶子结点中,因此叶子结点必须将所有键值及其对应的数据指针存储到磁盘文件块以便访问。此外,叶子结点被链接磁盘的某个位置,以提供对记录的有序访问。因此,叶子结点形成第一级索引,而内部结点形成多层索引的其他层。叶子结点的某些关键字 key 也出现在内部结点中,充当控制搜索记录的媒介。

与 B-树不同,B+树中的结点存在两个阶(order):对于阶 “a” 和 “ b”,一个用于内部结点,另一个用于外部(或叶)结点。

B+树定义

一棵 \(m\) 阶的 B+ 树可以这样定义:

  1. 每个节点最多可以有 \(m\) 个元素;
  2. 除了根节点外,每个节点最少有\(ceil(m/2)\)个元素
  3. 如果根节点不是叶节点,那么它最少有 2 个孩子节点;
  4. 所有的叶子节点都在同一层
  5. 一个有 k 个孩子节点的非叶子节点有 (k-1) 个元素,按升序排列;
  6. 某个元素的左子树中的元素都比它小,右子树的元素都大于或等于它;
  7. 非叶子节点只存放关键字和指向下一个孩子节点的索引,记录只存放在叶子节点中;
  8. 相邻的叶子节点之间用指针相连。

阶为 a 的 B+树内部结点的结构如下:

  1. 对于每一个形如:\(<P_1,K_1,P_2,K_2,......,P_{c-1},K_{c-1},P_c>\)的内部结点,其中\(c\le a\) ,每一个\(P_i\)表示指向子树根结点的指针,\(K_i\)表示关键字值
  2. 对于每一个内部结点中的关键字值均满足:\(K_1<k_2<=...\le k_{c-1}\).(内部结点的关键字由小到大有序排列)
  3. 对于一个位于 \(P_i\)所指向的子树中的结点\(X\)而言,满足:
    \(1<i<c\) 时,均有\(K_{i-1}<X\le K_i\) .
    \(i=c\)时,\(X>K_{c-1}\).
    \(i=1\)时,\(X\le K_1\).
  4. 每一个内部结点最多有a个指向子树的指针,即 c 最大取 a .
  5. 根结点至少包含两个指向子树的结点指针,即对于根结点而言\(2\ge c\le a\) ; 除了根之外的每个结点都包含最少\(ceil(a/2)\)个指向子树的指针。
  6. 如果任意一个内部结点包含 c 个指向孩子结点的指针且\(c\le a\),则该结点包含\(c-1\)的关键字。

阶为 b 的 B+树叶子结点的结构:

  1. 对于每一个形如:\(<<K_1,D_1>,<K_2,D_2>,......,<K_{c-1},D_{c-1}>,P_{next}>\)的叶子结点,其中\(c\le b\)\(D_i\)是一个数据指针(指向磁盘上的值等于\(K_i\)的真实记录的指针,或者包含记录\(K_i\) 的磁盘文件块),\(K_i\)是一个关键字,\(P_{next}\)表示 B+树中指向下一个叶子结点的指针。
  2. 对任意一个叶子结点均有:\(K_1<k_2<=...\le k_{c-1}\)\(c\le b\).
  3. 每一个叶子结点至少包含\(ceil(a/2)\) 个值.
  4. 所有的叶子结点在同一层。

使用 \(P_{next}\)指针可以遍历所有的叶子结点,就和单链表一样,从而实现对磁盘上记录的有序访问。

下图为一颗完整的 B+树的结构示例:

B+树的优点

同为\(h\)层的 B-树和 B+树,B+树可以存储更多的结点元素,更加 ”矮胖“。这也是 B+树最大的优势所在,极大地改善了 B-树的查找效率。对于同样多的记录,B+树的高度会更矮,并且\(P_{next}\)指针的出现可以帮助 B+树快速访问磁盘记录且效率非常高。总之,就是 B+树比 B-树更加好,B+树的磁盘 I / O 会更少,相比于 B-树的中序遍历,B+树只需要像遍历单链表一样扫描一遍叶子结点。

用心记录,持续成长