b树详细介绍与示例

B 树是一棵多路平衡查找树。每个节点可以放多个有序关键字,所有叶子都在同一层。数据库索引和文件系统用它,是因为一个节点正好对应磁盘上的一页:一次读入就能比较多个关键字,树变矮,磁盘次数就少。

下面用最小度数 t = 2 举例。这是教学里最常见的一种,也叫 2-3-4 树。

  • 一个节点最多 2t - 1 = 3 个关键字。
  • 除根以外,每个节点至少 t - 1 = 1 个关键字。
  • 有 k 个关键字的内部节点有 k + 1 个子节点。
  • 关键字从小到大排列。第 i 个子树里的值,都落在第 i-1 个和第 i 个关键字之间。

插入时怎么分裂

依次插入 10, 20, 5, 6, 12, 30, 7, 17。

节点没满时,只是按顺序插进去:

复制代码
[10] → [10, 20] → [5, 10, 20]

再插入 6 时,叶子已经有 3 个关键字,必须先分裂。中间的 10 升到父节点,左右各留一半:

复制代码
      [10]
     /    \
[5, 6]    [20]

12、30、7 都还能放进现有叶子。插入 17 时,右叶子 [12, 20, 30] 已满,中间的 20 升上去:

复制代码
         [10, 20]
       /    |     \
[5, 6, 7] [12, 17] [30]

查找 12:根上 10 < 12 < 20,走进中间那棵子树,在 [12, 17] 里命中。查找 99:从根走到最右边的 [30],叶子里没有,就确定不存在。整棵树的高度始终一致,不会像普通二叉搜索树那样一边倒。

删除时要维持"至少 t-1 个关键字"。节点不够就向兄弟借一个;兄弟也不够,就和兄弟以及父节点里的分隔关键字合并。插入的核心是满了就分裂,删除的核心是不够就借或合并。

复制代码
class BTreeNode:
    def __init__(self, leaf=True):
        self.leaf = leaf
        self.keys = []
        self.children = []


class BTree:
    def __init__(self, t):
        if t < 2:
            raise ValueError("t 必须 >= 2")
        self.t = t
        self.root = BTreeNode(leaf=True)

    def search(self, key, node=None):
        node = self.root if node is None else node
        i = 0
        while i < len(node.keys) and key > node.keys[i]:
            i += 1
        if i < len(node.keys) and key == node.keys[i]:
            return node, i
        if node.leaf:
            return None
        return self.search(key, node.children[i])

    def insert(self, key):
        root = self.root
        if len(root.keys) == 2 * self.t - 1:
            new_root = BTreeNode(leaf=False)
            new_root.children.append(root)
            self._split_child(new_root, 0)
            self.root = new_root
        self._insert_non_full(self.root, key)

    def _insert_non_full(self, node, key):
        i = len(node.keys) - 1
        if node.leaf:
            node.keys.append(None)
            while i >= 0 and key < node.keys[i]:
                node.keys[i + 1] = node.keys[i]
                i -= 1
            node.keys[i + 1] = key
            return
        while i >= 0 and key < node.keys[i]:
            i -= 1
        i += 1
        if len(node.children[i].keys) == 2 * self.t - 1:
            self._split_child(node, i)
            if key > node.keys[i]:
                i += 1
        self._insert_non_full(node.children[i], key)

    def _split_child(self, parent, i):
        t = self.t
        full = parent.children[i]
        right = BTreeNode(leaf=full.leaf)
        mid = full.keys[t - 1]
        right.keys = full.keys[t:]
        full.keys = full.keys[: t - 1]
        if not full.leaf:
            right.children = full.children[t:]
            full.children = full.children[:t]
        parent.keys.insert(i, mid)
        parent.children.insert(i + 1, right)


tree = BTree(2)
for k in [10, 20, 5, 6, 12, 30, 7, 17]:
    tree.insert(k)

print(tree.search(12) is not None)  # True
print(tree.search(99))               # None

数据库里更常见的是 B+ 树:内部节点只放索引用的关键字,真正的数据都在叶子,叶子之间再串成链表,方便按范围往后扫。

B 树和它的变体 B+ 树,主要用在「按关键字找一页数据,还要经常做范围扫描」的地方。一个节点对应磁盘或 SSD 上的一页,树矮,随机读的次数少。

数据库索引是现在最常见的用途。

  • MySQL InnoDB 的聚簇索引和二级索引都是 B+ 树,表数据就挂在主键索引的叶子上。
  • PostgreSQL 默认的 btree 索引、Oracle 的 B-tree 索引、SQL Server 的聚集/非聚集索引,结构都是 B+ 树。
  • SQLite 的表和索引也是 B 树。
  • MongoDB 的 WiredTiger 用 B+ 树存集合和索引。

文件系统的目录和元数据也大量用它。

  • XFS、NTFS、APFS 用 B+ 树管目录和分配信息。
  • btrfs 的核心结构就是写时复制的 B 树。
  • ext4 的大目录用 HTree,是按哈希分桶的 B 树。

嵌入式键值库里,LMDB、Berkeley DB 以 B+ 树做主结构。读多、数据要落在本地文件、还要范围查询时,这种结构很合适。

写非常多、可以接受后台合并的系统,常见主结构是 LSM-Tree,例如 RocksDB、LevelDB、Cassandra、HBase。分析型引擎如 StarRocks、Doris 的主键模型也不是用 B 树存整表,而是列存加上稀疏前缀索引。在线事务库的索引和文件系统元数据,目前仍然以 B+ 树为主。

相关推荐
开发小能手1239 小时前
答案daanC语言
数据结构·算法
启观川12 小时前
数据结构与算法-第 3 章 常用算法-贪心算法
数据结构·笔记·python·算法·贪心算法
程序员清风13 小时前
堆与优先队列:从 Top K 到任务调度
java·数据结构·算法
suhan13 小时前
【数据结构】四、串:串string、模式匹配BF\KMP、广义表
数据结构·后端·算法
啵啵啵鱼14 小时前
第二章 线性表
java·数据结构·算法
局部有解14 小时前
Hot100 - 三数之和
数据结构·c++·算法
辉艺棉麻小张14 小时前
用 JSON 建模面料规格:成分、克重、支数与公定回潮率字段设计
数据结构
longlongzihan14 小时前
C++ 常用数据结构速查:vector、stack、queue、list、树、图、哈希表等声明与核心操作
数据结构·c++·list
爱读书的小胖19 小时前
LeetCode 27. 移除元素(C)解题分析
数据结构·算法