公司动态

从数组到 B+ 树:一棵树的进化史

📅 2026/9/2 10:07:20
从数组到 B+ 树:一棵树的进化史
本文用因果链视角把二叉搜索树、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)二者不可兼得——查快则增删慢增删快则查慢。解法二叉搜索树BSTBST 的核心规则左子树所有节点 根 右子树所有节点。这个规则带来一个关键效果每次比较都能排除一半的搜索空间类似二分查找。查找从根开始比根小走左边比根大走右边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)但旋转次数大幅减少。五条性质每个节点是红色或黑色根节点是黑色叶子节点NULL是黑色红色节点的子节点必须是黑色即不能出现连续两个红色节点简称不红红从任一节点到其所有叶子节点的路径经过的黑色节点数相同简称黑路同为什么这五条规则能保证最长 ≤ 2×最短由性质 5所有路径的黑节点数相同设为 N由性质 4红节点不能连续出现 → 最长路径是黑红交替 2N最短路径是全黑 N所以最长 ≤ 2 × 最短为什么永远插入红色节点这是理解红黑树的入口问题如果插入黑色节点 → 这条路径多了一个黑节点 →破坏黑路同→ 必须调整如果插入红色节点 → 只可能破坏不红红如果父节点也是红色→ 如果父节点是黑色什么都不用做所以插入红色是破坏最小的选择。uncle 节点机制核心插入红色节点后如果父节点也是红色就出现了连续红冲突。怎么调整看 uncle父的兄弟是什么颜色——这是策略开关uncle 红色parent 和 uncle 变黑grandparent 变红grandparent 当作新的插入点递归向上检查因为 uncle 红说明 grandparent 的另一侧也有红节点可以安全地把两个红都变黑、grandparent 变红。但 grandparent 变红可能又和它的父冲突所以要向上递归。不需要旋转但可能传播。uncle 黑色或 NULL判断 LL / RR / LR / RL 型同 AVL执行旋转机械动作和 AVL 完全一样旋转后新根变黑旧根变红一次搞定不需要向上传播因为 uncle 黑说明另一侧没有多余的红节点可以匀过来所以必须通过旋转改变结构。旋转后一次性恢复所有性质。uncle 颜色策略旋转次数向上传播红纯变色0 次是可能多次黑或 NULL旋转 变色1 次否一次搞定调整策略动图演示情况 1uncle 黑色 → 旋转 变色LL 型右旋示例关键观察旋转后新根 P 变黑、旧根 G 变红一次搞定不再向上传播。和 AVL 旋转的机械动作完全一样区别只是多一步变色。情况 2uncle 红色 → 纯变色不旋转关键观察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 底层是红黑树TreeMapInteger,StringmapnewTreeMap();// O(log N) 增删查// HashMapJDK 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新 key 先插入到叶子节点按大小找到位置如果节点 key 数超过上限 →溢出取中间 key 上提到父节点左右两部分各成新节点如果父节点也溢出 → 继续向上分裂如果根节点也分裂 → 中间 key 成为新根树高 1口诀中间上提左右分裂逐层传播动态演示5 阶 B 树叶子 L 已装 4 个 key上限再插入 25 → 溢出。中间 key 25 上提到父节点左右各成新节点 [10,20] 和 [30,40]。父节点若也溢出 → 继续向上分裂逐层传播根节点也分裂时树高 1。B 树的删除下溢出 → 借位 / 合并前置步骤如果要删除的 key 在内部节点非叶子先用其前驱左子树最大值或后继右子树最小值替换它然后删除前驱/后继。前驱和后继一定在叶子节点所以问题降级为从叶子节点删除——和 BST 删除第 3 种情况的降级思路完全一致。删除后如果节点 key 数低于下限 →下溢出先看兄弟够不够借兄弟有富余key 数 最少值→父下来兄上去父节点对应的 key 下来补位兄弟的最大或最小key 上去当新父兄弟也不够借只有最少 key 数→合并父节点下来参与合并两个节点为一个然后检查父节点是否下溢出如果父也下溢出 → 递归向上口诀够借就借父下兄上不够就合父下来合逐层检查注意借位时不是直接拿兄弟的 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 树基础上做了两个关键改动数据全部放在叶子节点内部节点只存索引key叶子节点用双向链表串联动态演示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 的BETWEEN、ORDER BY、LIMIT都能高效实现。更稳定的查询效率每次查询都从根走到叶路径长度相同性能可预测便于系统优化。更高的缓存利用率非叶节点只存索引、结构紧凑内存有限时可以缓存更多非叶节点进一步减少 IO。延伸聚簇索引、二级索引与回表B 树在 MySQL InnoDB 中有两种形态这是面试高频考点聚簇索引Clustered Index叶子节点存的是完整行数据。主键索引就是聚簇索引。InnoDB 中数据本身以 B 树按主键组织——所以数据即索引索引即数据。二级索引Secondary Index叶子节点存的不是行数据而是主键值。你建的非主键索引都是二级索引。回表当你用二级索引查询但 select 的字段不在索引里时先在二级索引的 B 树中找到主键值再拿着主键值去聚簇索引的 B 树中查完整行数据这个过程叫回表多了一次 B 树查找。所以 SQL 优化里会建议用覆盖索引——查询的字段全在二级索引里就不用回表了。简单记忆主键索引叶子 整行数据非主键索引叶子 主键值。走非主键索引查非索引字段 多查一棵树 回表。深挖顺着叶子链表扫真的是顺序 IO吗前面反复说B 树范围查询 顺序 IO。这句话藏了三层没拆开的事实面试官一旦追问顺序到底顺序在哪这里就是分水岭。先澄清一个常见误解B 树首先是磁盘上的组织结构不是只存在于内存。InnoDB 的表空间.ibd 文件本身就是按 B 树组织的层级是页16KB→ 区extent1MB 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。面试被追问时三句话版本链表给的是逻辑顺序物理上页不一定相邻InnoDB 用 1MB 的区成片分配磁盘空间让逻辑相邻的页尽量物理相邻顺序插入的表基本能兑现成顺序 IO碎片化的表兑现不了此时靠线性预读默认 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、TreeSetTreeMapInteger,StringmapnewTreeMap();// O(log N) 增删查// 链表转红黑树 —— HashMap (JDK 1.8)// 当哈希桶链表长度 8 且数组长度 64 时链表转为红黑树// 当红黑树节点数 6 时退化为链表// 跳表 —— 高并发场景ConcurrentSkipListMapInteger,StringskipMapnewConcurrentSkipListMap();十、完整因果链复盘用费曼的方式过一遍整条链数组查快但增删慢链表增删快但查慢——因为二者不可兼得 →所以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_thresholdMySQL 官方文档 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含思维导图 覆盖索引讲解