从数组到 B+ 树:一棵树的进化史
2026/9/2 10:07:17 网站建设 项目流程

本文用因果链视角,把二叉搜索树、AVL 树、红黑树、B 树、B+ 树串成一条完整的演化线。每一环都是被上一环的副作用逼出来的——上一环的果,就是下一环的因。

一、为什么写这篇文章

面试时被问到"说说 B+ 树",大多数人能背出"数据全在叶子、叶子有链表、范围查询快"。但如果追问"那 B 树不行吗?"“红黑树为什么不行?”“AVL 树为什么不行?”——很多人就卡住了。

问题出在知识点是散的。单个结构能说几句,但结构之间的关系是断裂的。

本文的目标:用一条因果链把所有树结构串起来,让每一节"因为…所以…"都成立,拿掉任何一节后面的都站不住。

二、因果链总览

先看全景,再逐层展开。

起点问题终点核心解法
0数组 + 链表查快则增删慢,增删快则查慢,二者不可兼得BST左小右大的树形结构,查增删都 O(log N)
1BST顺序插入退化为链表,查找回退到 O(n)AVL强制左右子树高度差 ≤ 1,旋转恢复平衡
2AVL严格平衡导致旋转开销大,写入性能差红黑树放宽为"最长 ≤ 2×最短",最多 3 次旋转
3红黑树/AVL都是二叉树,百万数据 20 层 = 20 次磁盘 IOB 树多路搜索,一个节点存 N 个 key,树高压到 2-3 层
4B 树每个节点存数据,范围查询需中序遍历回溯B+ 树数据全放叶子 + 叶子链表串联,范围查询变扫描

记忆口诀:数组链表各有短 → 二叉搜索来补全 → 退化变链要平衡 → 少转几次红黑先 → 磁盘场景压扁它 → 范围查询加链表


三、第 0 层:数组 + 链表 → BST

问题

数据需要快速查找,也需要快速增删。但两种基本数据结构各有一个致命短板:

  • 数组:通过下标直接访问,查找 O(1);但插入/删除需要移动后面所有元素,O(n)
  • 链表:插入/删除只需改指针,O(1);但查找必须从头遍历,O(n)

二者不可兼得——查快则增删慢,增删快则查慢

解法:二叉搜索树(BST)

BST 的核心规则:左子树所有节点 < 根 < 右子树所有节点

这个规则带来一个关键效果:每次比较都能排除一半的搜索空间,类似二分查找。

  • 查找:从根开始,比根小走左边,比根大走右边,O(log N)
  • 插入:先查找定位,找到空位直接放入,O(log N)
  • 删除:三种情况(见下文),O(log N)

三者兼得——这就是 BST 的价值。

BST 的删除操作(3 种情况)

删除是 BST 中最复杂的操作,但它是后续所有树结构删除操作的基础原型

情况 1:无子节点→ 直接删除。叶子节点,删了不影响其他节点。

情况 2:只有一个子节点→ 子节点顶上。子树整体上移一位,BST 性质不变。

情况 3:有两个子节点→ 用直接后继(右子树中的最小值)替换被删节点的值,然后删除后继节点。

关键洞察:后继一定在右子树的最左下角,它最多只有一个右子节点。所以删后继的问题就降级为情况 1 或情况 2。这个"降级"思路在红黑树和 B 树的删除中反复出现。

BST 的致命问题:退化

BST 的一切优势都建立在"树是平衡的"这个假设上。但如果按 1→2→3→4→5→6→7 的顺序插入呢?

每个新节点都比前一个大,所以永远往右子树插。树变成了一条向右倾斜的链表——查找效率从 O(log N) 退化到 O(n)。

因为你不能假设用户总是按"理想顺序"插入数据 →所以退化问题真实存在 →所以必须引入自动平衡机制。

动态演示(自绘)

按 1、2、3、4、5 顺序插入时,每个新 key 都比前面大,永远往右走,最后退化成链表,查找 5 要比较 5 次。这就是第 1 层 AVL 要解决的核心问题。


四、第 1 层:BST 退化 → AVL 树

解法

AVL 树(以发明者 Adelson-Velsky 和 Landis 命名)给出的解法:任何节点的左右子树高度差不超过 1

这个差值叫平衡因子(Balance Factor),取值只能是 0、1、-1。一旦插入或删除导致某节点 |bf| > 1,立刻通过旋转恢复平衡。

旋转操作

旋转是 AVL 树的核心机制,所有复杂旋转都是两个基本旋转的组合:

右旋(LL 型):左子树的左子树插入导致失衡 → 把左子提上来当根,原根变成左子的右子。

左旋(RR 型):右子树的右子树插入导致失衡 → 把右子提上来当根,原根变成右子的左子。

LR 型:左子树的右子树插入导致失衡 → 先对左子左旋,再对根右旋。

RL 型:右子树的左子树插入导致失衡 → 先对右子右旋,再对根左旋。

口诀:哪个方向沉,就往反方向转,把沉的节点提上来

旋转动图演示

静态图看不出"谁先动、谁后动",下面是 3 个旋转的逐帧动画(自绘):

LL 型 → 右旋:插入导致左子树的左子树过高。

RR 型 → 左旋:插入导致右子树的右子树过高。

LR 型 → 先左后右双旋:插入在左子树的右子树。LL 和 RR 都不够用,必须先对左子左旋把它变成 LL 型,再对根右旋。

RL 型是 LR 的镜像(先右后左),本质一样。掌握这 3 个动画,其他都能类推。

插入 vs 删除的调整范围

操作调整范围原因
插入只需调整距离插入点最近的失衡节点插入只让子树高度 +1,旋转后高度恢复原值,上面的祖先自动恢复平衡
删除需要逐层向上检查每个祖先删除让子树高度 -1,旋转后高度可能仍然比原来矮 1,上面的祖先可能继续失衡

动态演示(自绘):一次删除触发两级级联旋转,直观展示"为什么删除要逐层向上检查"

关键差异:插入旋转后子树高度复原,调最近失衡点就完事;删除旋转后子树高度可能少 1,失衡会向上传染——所以必须逐层检查每个祖先。

这是 AVL 的一个重要代价:删除操作的最坏情况需要 O(log N) 次旋转——这也正是红黑树要解决的问题。

AVL 的问题

AVL 保证了查找一定是 O(log N),但代价也来了:严格平衡意味着每次插入都可能触发旋转,而且删除可能触发 O(log N) 次向上传播的旋转。高频写入场景吃不消。

因为AVL 旋转成本高 →所以需要一种"少转几次"的方案 →红黑树


五、第 2 层:AVL 旋转开销大 → 红黑树

核心思路

红黑树的核心思路:用"不那么严格"换"维护成本大幅降低"

AVL 要求高度差 ≤ 1,红黑树放宽为"最长路径 ≤ 2 × 最短路径"——查找仍然 O(log N),但旋转次数大幅减少。

五条性质

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 叶子节点(NULL)是黑色
  4. 红色节点的子节点必须是黑色(即不能出现连续两个红色节点,简称"不红红")
  5. 从任一节点到其所有叶子节点的路径,经过的黑色节点数相同(简称"黑路同")

为什么这五条规则能保证"最长 ≤ 2×最短"?

  • 由性质 5,所有路径的黑节点数相同(设为 N)
  • 由性质 4,红节点不能连续出现 → 最长路径是黑红交替 = 2N
  • 最短路径是全黑 = N
  • 所以最长 ≤ 2 × 最短

为什么永远插入红色节点?

这是理解红黑树的入口问题

  • 如果插入黑色节点 → 这条路径多了一个黑节点 →破坏"黑路同"→ 必须调整
  • 如果插入红色节点 → 只可能破坏"不红红"(如果父节点也是红色)→ 如果父节点是黑色,什么都不用做

所以插入红色是破坏最小的选择。

uncle 节点机制(核心)

插入红色节点后,如果父节点也是红色,就出现了"连续红"冲突。怎么调整?看 uncle(父的兄弟)是什么颜色——这是策略开关:

uncle = 红色

  1. parent 和 uncle 变黑
  2. grandparent 变红
  3. grandparent 当作新的插入点,递归向上检查

因为 uncle 红说明 grandparent 的另一侧也有红节点,可以安全地把两个红都变黑、grandparent 变红。但 grandparent 变红可能又和它的父冲突,所以要向上递归。不需要旋转,但可能传播。

uncle = 黑色(或 NULL)

  1. 判断 LL / RR / LR / RL 型(同 AVL)
  2. 执行旋转(机械动作和 AVL 完全一样)
  3. 旋转后:新根变黑,旧根变红
  4. 一次搞定,不需要向上传播

因为 uncle 黑说明另一侧没有多余的红节点可以"匀"过来,所以必须通过旋转改变结构。旋转后一次性恢复所有性质。

uncle 颜色策略旋转次数向上传播
纯变色0 次是,可能多次
黑(或 NULL)旋转 + 变色1 次否,一次搞定

调整策略动图演示

情况 1:uncle = 黑色 → 旋转 + 变色(LL 型右旋示例)

关键观察:旋转后新根 P 变黑、旧根 G 变红,一次搞定,不再向上传播。和 AVL 旋转的机械动作完全一样,区别只是多一步"变色"。

情况 2:uncle = 红色 → 纯变色,不旋转

关键观察:P 和 U 同时变黑、G 变红(黑高守恒)。G 是根则强制变黑;如果 G 不是根,要把它当新插入点继续向上检查——这正是红黑树可能向上传播的根源。

两种策略的因果:uncle 红说明"对面也有红可匀",所以只调颜色;uncle 黑说明"对面没货可匀",必须改结构。少传 1 次 vs 少转 1 次的取舍,就是红黑树维护成本低的核心。

删除的"双黑"概念

删除一个黑色叶子节点时,那条路径少了一个黑节点,破坏"黑路同"。解法是把这个位置标记为**“双黑”**(逻辑上多算一个黑),然后逐步消除:

情况操作
双黑的兄弟是黑色且至少有一个红孩判断 LL/RR/LR/RL → 旋转 + 变色,双黑变单黑 → 结束
双黑的兄弟是黑色全黑孩兄弟变红,双黑上移到父节点 → 继续修复
双黑的兄弟是红色兄父变色 + 父节点朝双黑方向旋转 → 继续按上面两种情况处理

动态演示:兄弟黑 + 有红孩 → 旋转修复(最常见的一次性修复场景):

删除黑色叶子 20 后,左路径少一个黑 → 记为「双黑」。S 是黑色且有红孩 SR → 一次旋转 + 变色搞定:S 继承父色(红),G 变黑,红孩 SR 变黑,双黑被吸收,不再向上传播

AVL vs 红黑树

维度AVL红黑树
平衡条件高度差 ≤ 1(严格)最长 ≤ 2×最短(宽松)
查找效率略快(树更矮)略慢但仍是 O(log N)
插入旋转次数最坏 O(log N)最多 3 次
删除旋转次数最坏 O(log N)最多 3 次
适用场景查找密集增删频繁

因为红黑树用颜色代替了高度差检查 →所以旋转触发频率大幅降低 → 维护成本比 AVL 低一个数量级。

Java 中的红黑树

// TreeMap、TreeSet 底层是红黑树TreeMap<Integer,String>map=newTreeMap<>();// O(log N) 增删查// HashMap(JDK 1.8+):当哈希桶链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树// 这是为了防止哈希冲突严重时链表查找退化到 O(n)

六、第 3 层:二叉树磁盘 IO 高 → B 树

问题

到这里为止,AVL 和红黑树解决了内存内的查找效率问题。但一旦数据要落盘,新问题来了。

所有二叉树每个节点只有 2 个叉 → 树高 = log₂(N) → 百万数据 ≈ 20 层 → 每次查找要 20 次磁盘 IO。

磁盘 IO 的瓶颈不在 CPU 计算,在寻道时间——磁头移动到对应磁道的物理时间。看一个量化对比:

存储层级访问延迟相对比例
L1 缓存~1 ns1×(基准)
内存(RAM)~100 ns100×
SSD~100,000 ns (0.1 ms)100,000×
机械硬盘(HDD)~10,000,000 ns (10 ms)10,000,000×

一次磁盘 IO 的时间,够 CPU 执行上千万条指令。在内存里多算几步不叫浪费,多读一次磁盘才是真正的瓶颈。

此外,磁盘是按块读取的——读 1 字节和读 1 整块(4KB~16KB)的时间差不多。这意味着:如果一个节点只存一个 key,磁盘上那一整块里剩下的空间就白白浪费了。反过来,如果一个节点正好填满一整块,一次 IO 拿到大量 key,性价比最高。

因为瓶颈从 CPU 转移到了磁盘 →所以优化的目标不是"少转几次",而是"少读几层" + “每层多拿几个 key” → 思路是横向扩展每个节点,让一个节点存更多 key,把树压矮。

解法:B 树

B 树是一种多路平衡搜索树,三个核心特性:

  • 多路:每个节点可以有多个子节点(不是只有 2 个)
  • 平衡:所有叶子节点在同一层
  • 搜索:节点内的 key 有序排列

一棵 m 阶 B 树的性质:

  • 每个节点最多 m 个子节点,最多 m-1 个 key
  • 每个非根非叶节点至少 ⌈m/2⌉ 个子节点
  • 根节点至少 2 个子节点(除非整棵树就一个节点)

关键设计:一个节点的大小恰好填满一个磁盘页(通常 4KB/16KB)。一次 IO 读整页,拿到大量 key。

效果:百万数据只需 2-3 层 → 2-3 次磁盘 IO,直接砍掉 90%。

B 树的查找

和 BST 非常像,区别是每个节点不只有一个 key。在节点内可以用折半查找快速定位,然后决定走哪个子节点。

B 树的插入:溢出 → 分裂

以 5 阶 B 树为例(每节点最多 4 个 key,最少 2 个 key):

  1. 新 key 先插入到叶子节点(按大小找到位置)
  2. 如果节点 key 数超过上限 →溢出
  3. 中间 key 上提到父节点,左右两部分各成新节点
  4. 如果父节点也溢出 → 继续向上分裂
  5. 如果根节点也分裂 → 中间 key 成为新根,树高 +1

口诀:中间上提,左右分裂,逐层传播

动态演示(5 阶 B 树)

叶子 L 已装 4 个 key(上限),再插入 25 → 溢出。中间 key 25 上提到父节点,左右各成新节点 [10,20] 和 [30,40]。父节点若也溢出 → 继续向上分裂(逐层传播),根节点也分裂时树高 +1。

B 树的删除:下溢出 → 借位 / 合并

前置步骤:如果要删除的 key 在内部节点(非叶子),先用其前驱(左子树最大值)或后继(右子树最小值)替换它,然后删除前驱/后继。前驱和后继一定在叶子节点,所以问题降级为"从叶子节点删除"——和 BST 删除第 3 种情况的降级思路完全一致。

  1. 删除后如果节点 key 数低于下限 →下溢出
  2. 先看兄弟够不够借
    • 兄弟有富余(key 数 > 最少值)→父下来兄上去:父节点对应的 key 下来补位,兄弟的最大(或最小)key 上去当新父
    • 兄弟也不够借(只有最少 key 数)→合并:父节点下来参与合并两个节点为一个,然后检查父节点是否下溢出
  3. 如果父也下溢出 → 递归向上

口诀:够借就借(父下兄上),不够就合(父下来合),逐层检查

注意:借位时不是直接拿兄弟的 key 过来,而是"父下来兄上去"——这样才能保持 B 树的有序性。

动态演示:兄弟有富余 → 借位(父下来兄上去)

删除 40 后 M 只剩 1 个 key < 下限 2 → 下溢出。右兄有富余 → 借位:父 key 60 下来补位,兄弟最小 key 70 上去当新分隔 key。注意:必须经父节点中转,直接搬 70 会破坏有序性。

动态演示:兄弟也没富余 → 合并(父下来合)

两个兄弟都只有下限 2 个 key → 没得借 → 合并:父 key 30 下来,左兄 + M 合成 [10,20,30,50]。父节点因此少了 1 个 key → 检查父节点是否也下溢出(这就是删除调整可能逐层向上传播的根源)。

B 树的问题

B 树每个节点都存数据。如果要做范围查询(比如查 key 在 [20, 50] 之间的所有记录),需要做中序遍历——在节点之间来回跳,不断回溯,产生大量随机磁盘 IO。

因为范围查询是数据库最高频操作 →所以必须优化这一步 →B+ 树


七、第 4 层:B 树范围查询差 → B+ 树

解法

B+ 树在 B 树基础上做了两个关键改动:

  1. 数据全部放在叶子节点,内部节点只存索引(key)
  2. 叶子节点用双向链表串联

动态演示:B+ 树范围查询 [40, 70]

第①步从根定位到中间叶子 P2(路径等长、稳定)。第②步叶子内命中 40、50,扫完沿链表右移。第③步下一叶子命中 60、70,遇到 80 > 70 停止。全程一次定位 + 顺链表顺序 IO,这正是 B+ 树成为数据库索引首选的原因。

B 树 vs B+ 树详细对比

特性B 树B+ 树
数据存储所有节点都可能存数据仅叶子节点存数据,非叶只存索引
叶子结构叶子之间独立叶子用双向链表串联
非叶节点数据不能同时存在于非叶和叶非叶的 key 是子树最大值,同一 key 可同时存在于多级非叶和叶
查找效率可能在非叶命中,效率不稳定必须走到叶子,效率稳定
范围查询中序遍历,大量随机 IO顺着叶子链表扫,顺序 IO
空间利用非叶存数据,节点"胖"非叶只存索引,节点"瘦" → 同样磁盘页装更多 key →树更矮

B+ 树的缺点

没有银弹。B+ 树也有代价:

  • Key 冗余:内部节点的 key 是叶子节点 key 的副本。同一个 key 可能出现在多级索引节点中,浪费空间。不过实际中,索引节点占用的空间远小于叶子数据节点,所以这个代价通常可以接受。
  • 无法在非叶命中:即使要查的数据刚好是内部节点的 key,也必须走到叶子才能拿到数据。B 树如果在非叶节点就找到了,可以提前返回。不过这个代价换来了查询效率的稳定可预测——对所有 key 都一视同仁。

权衡:B+ 树牺牲了一点空间(key 重复)和一点"运气好时"的查找速度,换来了范围查询的革命性提升和查询效率的绝对稳定。对数据库来说,这笔买卖太划算了。

为什么 B+ 树更适合数据库

更优的磁盘 IO:非叶节点不存数据 → 更小 → 一个磁盘页装更多索引 → 树更矮 → IO 更少。

以 MySQL InnoDB 为例(默认页大小 16KB),做一个具体计算:

节点类型每个 key+指针 大小一个 16KB 页能装
非叶节点(索引页)~16 字节(key 8B + 子指针 8B)约 1000 个索引项
叶子节点(数据页)~160 字节(key + 一行数据)约 100 条记录

三层 B+ 树的容量 = 1000 × 1000 × 100 =1 亿条记录,仅需 3 次磁盘 IO。

反观二叉树:1 亿条记录 → 树高 ≈ log₂(10⁸) ≈ 27 层 →27 次磁盘 IO

B+ 树用 3 次 IO 干掉了二叉树 27 次 IO——这就是多路 + 非叶不存数据的威力。

革命性的范围查询:定位到范围下界后,顺着叶子链表一路扫到底。SQL 的BETWEENORDER BYLIMIT都能高效实现。

更稳定的查询效率:每次查询都从根走到叶,路径长度相同,性能可预测,便于系统优化。

更高的缓存利用率:非叶节点只存索引、结构紧凑,内存有限时可以缓存更多非叶节点,进一步减少 IO。

延伸:聚簇索引、二级索引与回表

B+ 树在 MySQL InnoDB 中有两种形态,这是面试高频考点:

聚簇索引(Clustered Index):叶子节点存的是完整行数据。主键索引就是聚簇索引。InnoDB 中,数据本身以 B+ 树按主键组织——所以"数据即索引,索引即数据"。

二级索引(Secondary Index):叶子节点存的不是行数据,而是主键值。你建的非主键索引都是二级索引。

回表:当你用二级索引查询,但 select 的字段不在索引里时:

  1. 先在二级索引的 B+ 树中找到主键值
  2. 再拿着主键值去聚簇索引的 B+ 树中查完整行数据

这个过程叫"回表",多了一次 B+ 树查找。所以 SQL 优化里会建议用覆盖索引——查询的字段全在二级索引里,就不用回表了。

简单记忆:主键索引叶子 = 整行数据,非主键索引叶子 = 主键值。走非主键索引查非索引字段 = 多查一棵树 = 回表。

深挖:顺着叶子链表扫,真的是"顺序 IO"吗?

前面反复说"B+ 树范围查询 = 顺序 IO"。这句话藏了三层没拆开的事实,面试官一旦追问"顺序到底顺序在哪",这里就是分水岭。

先澄清一个常见误解:B+ 树首先是磁盘上的组织结构,不是"只存在于内存"。

InnoDB 的表空间(.ibd 文件)本身就是按 B+ 树组织的,层级是:页(16KB)→ 区(extent,1MB = 64 个连续页)→ 段(segment)→ 表空间。每个索引占两个段:叶子节点放数据段,非叶节点放索引段——段分开存,就是为了全表扫描/范围查询时只碰叶子段,不跟非叶页混在一起产生额外随机 IO。Buffer pool 里的页只是磁盘页的缓存副本,内存中并没有另一棵独立的 B+ 树。

那"顺序"到底在哪一层成立?拆成三层看:

层面"顺序"是什么由谁保证什么时候失效
页内16KB 页里记录连续存放,扫描页内记录是真·顺序内存访问页格式(记录按主键有序紧凑排列)几乎不失效
内存(buffer pool)无顺序可言——页读进内存后放哪个 16KB frame 由 free list / LRU 驱逐决定,跟页的逻辑编号无关无需保证:内存随机访问约 100ns,比磁盘寻道便宜 10 万倍,"跳着走"不构成瓶颈——
磁盘(IO 层)逻辑相邻的叶子页物理上也尽量相邻,读盘变成顺序读区(extent)分配+线性预读随机插入/删除/页分裂造成碎片

两个关键机制撑起了磁盘层的顺序性:

① 区(extent)分配——主动"争取"物理连续。如果按单页分配空间,链表上逻辑相邻的两页可能物理上相距很远,范围查询就退化成随机 IO。所以 InnoDB 以 64 页(1MB)为单位成片分配,大表增长时一次申请 4~5 个区,顺序插入场景下叶子页天然物理相邻。逻辑顺序是链表给的,物理顺序是 extent 争取的——这是两件事,B+ 树的链表本身并不承诺物理连续。

② 线性预读(linear read-ahead)——检测到顺序访问模式就整区预取。参数innodb_read_ahead_threshold(默认 56):一个区的 64 页中被连续访问了 ≥56 页,InnoDB 就异步预读下一个完整的区。等于说 InnoDB 不赌物理布局,它还在运行期持续观察访问模式,把"看起来在顺序扫"这个信号兑换成批量 IO,交给 OS 合并成大块顺序读。

反例最能说明问题:一张严重碎片化的大表做全表扫描——顺着叶子链表走,读完页 3 下一个是页 5230,早已不在同一个区,线性预读直接失效,"链表扫描"实际退化成一堆 16KB 随机读。DBA 社区著名的 Logical Read Ahead 优化,思路就是先批量读非叶页收集叶子页号、按页号(物理地址)排序后再批量读,把随机读重新变回顺序读,实测全表扫描提速约 10 倍。这反过来说明:顺序 IO 的收益来自物理局部性,不是链表结构自动带来的

碎片是顺序性的天敌,官方文档对碎片的定义就是:“索引页在磁盘上的物理顺序与页的索引顺序不接近,或 64 页块内存在过多未使用页”。顺序追加 + 从尾部删除的负载不会碎片化;随机插入/删除会。碎片严重时的解法就是OPTIMIZE TABLE重建整棵树,让叶子页重新物理连续。

顺带澄清一个容易想偏的推断:buffer pool 确实是启动时一次性 malloc/mmap 的大块内存,但页在内存里的摆放位置与逻辑顺序无关(由 free list 和 LRU 决定,定位靠 space_id + page_no 的 page hash),所以"申请大块内存 → 逻辑连续的页在内存里也挨着"并不成立。内存层根本不需要顺序——它快到不在乎跳着访问。

因果链收拢:因为叶子链表只保证逻辑顺序 → 所以物理连续必须靠 extent 分配主动争取 → 因为碎片会侵蚀物理连续 → 所以需要线性预读运行期补偿、OPTIMIZE TABLE 定期重建 → 所以"B+ 树范围查询是顺序 IO"的准确表述是:叶子链表提供逻辑连续性,extent 分配 + 线性预读把逻辑连续尽量兑换成物理顺序 IO

面试被追问时,三句话版本:

  1. 链表给的是逻辑顺序,物理上页不一定相邻;
  2. InnoDB 用 1MB 的区成片分配磁盘空间,让逻辑相邻的页"尽量"物理相邻,顺序插入的表基本能兑现成顺序 IO;
  3. 碎片化的表兑现不了,此时靠线性预读(默认 56/64 页触发)批量预取来救,救不动就只能 OPTIMIZE TABLE 重建。

八、补齐:其他常考的树

一句话定位和主链的关系
2-3-4 树红黑树的数学等价模型。2-3-4 树的每个 4-node 裂解为红黑树的红黑父子节点红黑树的理论基底
跳表(Skip List)多层链表 + 随机层数,概率性平衡。Redis ZSet 用它代替红黑树,因为并发友好(不需要全局旋转)红黑树的并发替代品
LSM 树“先写日志再批量合并”,牺牲读性能换写性能。LevelDB/RocksDB/HBase 都在用B+ 树的写入优化版
Trie(前缀树)按字符拆分 key 存成多叉树。适合前缀匹配、自动补全、IP 路由字符串场景的专用结构

九、Java 实战速记

// 红黑树 —— TreeMap、TreeSetTreeMap<Integer,String>map=newTreeMap<>();// O(log N) 增删查// 链表转红黑树 —— HashMap (JDK 1.8+)// 当哈希桶链表长度 >= 8 且数组长度 >= 64 时,链表转为红黑树// 当红黑树节点数 <= 6 时,退化为链表// 跳表 —— 高并发场景ConcurrentSkipListMap<Integer,String>skipMap=newConcurrentSkipListMap<>();

十、完整因果链复盘

用费曼的方式过一遍整条链:

数组查快但增删慢,链表增删快但查慢——因为二者不可兼得 →所以BST 用"左小右大"的树形结构让查增删都 O(log N)。

BST 顺序插入会退化成链表 →因为不能假设用户总是理想顺序插入 →所以AVL 强制高度差 ≤ 1,用旋转保证平衡。

AVL 严格平衡导致每次增删都可能触发旋转,删除最坏 O(log N) 次旋转 →因为旋转成本高 →所以红黑树放宽为"最长 ≤ 2×最短",用颜色规则代替高度差检查,插入永远插红,uncle 红→变色递归、uncle 黑→旋转一次搞定,最多 3 次旋转。

以上都是二叉树,百万数据 20 层 = 20 次磁盘 IO →因为瓶颈在磁盘不在 CPU →所以B 树横向扩展,一个节点存 N 个 key,树压到 2-3 层,一次 IO 读整页。

B 树每个节点存数据,范围查询要中序遍历回溯 →因为范围查询是数据库最高频操作 →所以B+ 树把数据全放叶子 + 链表串联,范围查询从"遍历树"变成"扫描链表",非叶只存索引让树更矮 IO 更少。

每一层的"问题"就是上一层的"解法"带来的副作用。上一环的果,就是下一环的因——这整条链没有一节是可以拿掉的。


参考资料

  • B 站 UP 主 蓝不过海呀 —— 数据结构动画讲解(背景阅读)
  • 《算法导论》第 12-18 章
  • MySQL 官方文档 —— InnoDB B+ 树索引实现
  • MySQL 官方文档 Configuring InnoDB Buffer Pool Prefetching (Read-Ahead) —— 线性/随机预读机制与 innodb_read_ahead_threshold
  • MySQL 官方文档 How MySQL Uses Memory —— buffer pool 启动时 malloc 分配
  • How to Understand InnoDB Extent and Segment Structure —— 表空间/段/区/页四级结构与碎片管理
  • Making full table scan 10x faster in InnoDB —— 碎片化大表全表扫描的随机 IO 实测与 Logical Read Ahead 优化
  • 详述 MySQL 中 InnoDB 的索引结构以及使用 B+ 树实现索引的原因 — 腾讯云开发者社区,段/区/页结构
  • Why MySQL Chooses B+ Trees: From BSTs to High-Performance Indexes — 完整演化链条 + InnoDB 容量计算
  • B树和B+树的插入、删除图文详解 — 阿里云开发者社区,含完整操作示例
  • 从二叉树到B+树:深入解析四大核心数据结构 — 博客园,含旋转图解
  • B-Trees Explained Visually — 交互式可视化 + 磁盘延迟对比数据
  • 【算法突围 02】树形结构与数据库索引 — CSDN,含思维导图 + 覆盖索引讲解

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询