B/B+树

1.树层高的影响

1. 核心影响:I/O 次数的倍增(最致命的影响)

图片左下角红圈里写着一句很关键的话:“每次对比后找下一个节点,是一次磁盘寻址。

在内存(RAM)中,指针跳转非常快(纳秒级),但在磁盘(Disk)上,情况完全不同:

  • 节点分散存储:在二叉树(Binary Tree)中,节点 Node A 和它的子节点 Node B 物理上可能存储在磁盘相距很远的地方。
  • 层高即 I/O
    • 如果树高是 10 层(如图片所示,1023 个节点)。
    • 最坏情况下,你需要读取 10 次磁盘 才能找到叶子节点的数据。
    • 数学账:机械硬盘的一次随机寻址(Random Seek)大约需要 10ms
    • 结果:10 层 $\times$ 10ms = 100ms。对于 CPU 来说,100ms 就像是一万年那么久。仅仅为了找一个数据就要卡顿 0.1 秒,这在并发系统中是不可接受的。

2. 数据量的指数级差异

二叉树的问题在于它是“瘦高”的。随着数据量增加,层高增长很快:

  • 数据量 100万
    • 二叉树:高度约为 20 层 (2 ^ 20 = 100W)。需要 20 次 I/O。
    • 问题:每次查询都要 20 次磁盘读取,数据库基本瘫痪。

为了解决这个问题,我们需要把树“压扁”,这就引出了 B树 / B+树(多路搜索树):

  • B+树(数据库常用)
    • 它不是二叉(2路),而是多路(比如 m=1000 阶)。一个节点可以存 1000 个索引。
    • 数据量 100万
      • 第一层:1 个节点(存 1000 个指针)。
      • 第二层:1000 个节点(每个存 1000 个数据)。
      • 总容量:$1000 \times 1000 = 100万$。
    • 层高:仅仅 2 层
    • 结果:只需要 2 次 I/O(甚至根节点常驻内存,只需要 1 次 I/O)。性能提升了 10 倍以上。

3. 系统资源的消耗(Context Switch & Page Faults)

除了慢,层高太高还会消耗系统资源:

  • 缺页中断(Page Fault):每访问下一层的一个新节点,操作系统可能都需要从磁盘加载一个新的“页(Page 4KB)”到内存中。层高越高,触发缺页中断的频率越高。
  • CPU 缓存失效:如果树太高且节点分散,CPU 的 L1/L2 缓存很难命中,导致 CPU 等待数据的时间变长。

4. 总结:为什么要“降层高”?

图片中的“降层高的数据结构”,指的就是从 “瘦高”的二叉树 转向 “矮胖”的 B+树

特性 二叉树 (Binary Tree) B+树 (B+ Tree)
形状 瘦高 矮胖
分支数 2 几十到上千 (取决于页大小)
100万数据层高 ~20 层 ~3 层
磁盘 I/O 次数 多 (慢) 少 (快)
适用场景 纯内存计算 (如 std::map) 磁盘存储 (如 MySQL, 文件系统)

2.多叉树和B树的区别

简单的一句话总结:多叉树(Multi-way Tree)是一个宽泛的“类别”,而 B树(B-Tree)是这个类别中一种“特种部队”,它有着严格的纪律(限制条件)来保证性能。

我们可以把它们的关系理解为:“多叉树”是原材料,而“B树”是经过精密设计的工程产品。

以下是具体的区别对比:

1. “平衡性”是最大的区别

这是 B树最核心的特征,也是它被称为 B-Tree (Balanced Tree) 的原因。

  • 普通多叉树
    • 很随意。它允许树长得“歪瓜裂枣”。
    • 例如:一个 5 叉树,可能所有数据都挂在最左边的子树上,导致左边有 100 层,右边只有 1 层。
    • 后果:查找效率无法保证。在最坏情况下,它会退化成类似链表的结构,查询很慢。
  • B树
    • 绝对平衡。它强制要求 所有的叶子节点必须在同一层
    • 后果:无论怎么插入或删除数据,B树的高度永远是均匀的(矮胖),保证了最坏情况下的查询效率也是 O(log N)。

2. 节点利用率(空间浪费问题)

  • 普通多叉树
    • 没有规定一个节点必须存多少数据。
    • 这就导致一个问题:如果你定义了一个 1000 叉的树,但大部分节点里只存了 1 个数据,空间就极度浪费,而且树会变得很高,起不到“降层高”的作用。
  • B树
    • 有最低填充要求。如果这是一棵 m 阶的 B树,它规定除根节点外,每个节点至少要有 [m / 2] 个子树。
    • 例如:在 1000 阶 B树中,每个节点至少要存约 500 个数据。如果删得太少,它会强制合并节点。
    • 目的:确保树总是“饱满”的,让树尽可能地“矮”。

3. 生长方式(向下 vs 向上)

这是 B树设计最天才的地方。

  • 普通多叉树
    • 向下生长。像我们在写二叉树代码时一样,插入数据时,如果当前节点满了或者符合条件,就new一个新的子节点挂在下面。树是越长越深。
  • B树
    • 向上生长
    • 插入数据时,是先往叶子节点里“塞”。
    • 当叶子节点塞满了(超过阶数限制),它会发生分裂(Split),把中间的一个节点挤上去(提拔到父节点)。
    • 如果父节点也满了,继续分裂,直到根节点分裂,树才会增加一层高度。
    • 这就是 B树能保持绝对平衡的秘诀。

总结对比表

特性 普通多叉树 (Multi-way Tree) B树 (B-Tree)
平衡性 不保证,可能严重倾斜 严格自平衡,所有叶子同一层
查找效率 不稳定,取决于树的形状 稳定,非常快 ($O(\log N)$)
空间利用率 可能很低(节点空荡荡) (规定了至少半满)
节点分裂/合并 只有简单的插入/删除 有复杂的自动分裂和合并机制
主要用途 内存中的特定算法(如字典树 Trie) 磁盘存储(数据库索引、文件系统)

3.B树的性质

1. 节点的子节点数量(上下限控制)

这是为了保证树既不过于拥挤,也不过于空闲。

  • 根节点:如果不是叶子节点,至少有 2 个子节点。
  • 非根节点(内部节点):
    • 最多:可以有 $m$ 个子节点。
    • 最少:必须有 $\lceil m/2 \rceil$ 个子节点。($\lceil \rceil$ 表示向上取整)。
    • 通俗理解:除根节点外,所有节点至少要是“半满”的。比如 5 阶 B树,每个节点至少要有 3 个叉,不能只有 1 个或 2 个。这避免了空间浪费和树变成“瘦高”的形状。

2. 节点的关键字(Key)数量

关键字就是我们存储的索引值(比如主键 ID)。关键字的数量与子节点的数量是严格绑定的。

  • 如果一个节点有 $k$ 个子节点,那么它必须包含 $k-1$ 个关键字。
  • 范围
    • 最多:$m-1$ 个关键字。
    • 最少:$\lceil m/2 \rceil - 1$ 个关键字。

3. 绝对平衡(最重要的一条)

  • 所有叶子节点必须在同一层。
  • 这意味着从根节点到任何一个叶子节点的路径长度都是完全一样的。
  • 对比:二叉平衡树(AVL)只要求高度差不超过 1,而 B树要求高度差必须为 0。

4. 节点内的有序性

  • 每个节点内部的关键字是从小到大排序的。
  • 比如一个节点里的关键字是 [10, 20, 30],那么:
    • 指向第一个子树的指针,对应的数都 < 10
    • 指向第二个子树的指针,对应的数都在 10 到 20 之间
    • 指向第三个子树的指针,对应的数都在 20 到 30 之间
    • ...以此类推。这保证了查找时可以使用类似二分查找的方法。

5. 生长与收缩机制

虽然这不是静态性质,却是维持上述性质的动态规则:

  • 分裂:当一个节点满了(关键字达到 $m-1$),再插入数据时,它会从中间切开,分裂成两个节点,并将中间的数提拔到父节点中。
  • 合并:当一个节点删空了(关键字少于 $\lceil m/2 \rceil - 1$),它会尝试向兄弟借节点,或者与兄弟合并,并将父节点的一个关键字拉下来。

4. B树代码

// 最小度
#define SUB_M       3

typedef struct _btree_node {

    int *keys; // 5
    struct _btree_node **childrens; // 6

    int num;
    int leaf;

} btree_node;


struct _btree {

    struct _btree_node *root;

};

5.B树和B+树的区别

1. 数据存在哪里?(这是最大的区别)

  • B树
    • “雨露均沾”。无论是根节点、内部节点还是叶子节点,都存储真实的数据(比如数据库中的一行记录,或者文件数据的物理地址)。
    • 后果:内部节点非常“重”。因为存了数据,一个磁盘页(Page,比如 16KB)能存下的节点数量就少了,导致树的“分叉”变少,树可能会变高。
  • B+树
    • “只有叶子才是最终归宿”
    • 非叶子节点(内部节点):只存索引(Key)和指针,不存数据。它们仅仅是“路标”。
    • 叶子节点:存储所有的真实数据。
    • 后果:因为内部节点不存数据,变得非常“轻”,一个磁盘页能塞下更多的 Key。这意味着 B+树比 B树更矮、更胖,I/O 次数更少。

2. 叶子节点是否相连?(范围查询的神器)

  • B树
    • 叶子节点之间是独立的,没有指针相连。
    • 痛点:如果你要查“ID 在 10 到 100 之间的数据”。在 B树中,你查到 10 以后,需要回到父节点,再去找 11,再找 12... 这是一种中序遍历,在磁盘上跳来跳去,效率很低。
  • B+树
    • 所有叶子节点通过双向链表(Doubly Linked List)连在一起。
    • 优势:范围查询极其快。
    • 过程:先从根找到“10”所在的叶子节点,然后顺着链表指针往后“跑”,直到遇到“100”为止。这叫全表扫描范围扫描

3. 查询性能的稳定性

  • B树
    • 不稳定。如果你要找的数据刚好在根节点,1 次 I/O 就找到了(最好的情况);如果在叶子节点,可能要 3 次 I/O。
  • B+树
    • 非常稳定。因为所有数据都在叶子层,无论查哪个数,都需要从根走到叶子。虽然看起来没 B树“最好情况”快,但对于数据库来说,可预期的延迟(Predictable Latency)比偶尔的快速更重要。

图解对比

假设我们要存一个节点大小为 16KB 的页:

B树的节点结构:

Plaintext

[ Key | Data | Key | Data | Key | Data ] 
  • Data 占地方,一个节点只能存 100 个 Key。树高可能是 4 层。

B+树的节点结构:

Plaintext

内部节点: [ Key | Ptr | Key | Ptr | Key | Ptr ]  <-- 只有路标
叶子节点: [ Key | Data | Key | Data ] --> [ Next Leaf ]
  • 内部节点不存 Data,一个节点能存 1000 个 Key。树高只有 3 层。

总结:为什么数据库(MySQL)选 B+树?

特性 B树 B+树 (MySQL InnoDB)
单点查询 可能更快 (如果数据在高层) 稳定 (必须走到叶子)
范围查询 (BETWEEN, >) (需要回溯遍历) 极快 (叶子链表顺推)
树的高度 较高 (胖度受限于数据大小) 更矮 (内部节点能塞更多索引)
磁盘预读 不友好 非常友好 (顺序读叶子)

结论

  • 如果你的场景是 Key-Value 存储(如 MongoDB 的某些模式),只需要查单个 Key,不需要范围查,B树 是可以的。
  • 如果你的场景是 关系型数据库(MySQL、Oracle),需要做 ORDER BY>< 等操作,B+树 是绝对的王者。
posted @ 2025-12-20 20:31  belief73  阅读(1)  评论(0)    收藏  举报