Theory · Data Structure · Balanced Tree

平衡树:AVL · 红黑树 · B+树

给 BST 按住高度:严格平衡(AVL)· 松弛规则(红黑)· 磁盘友好(B+)—— 内存与磁盘两套平衡哲学

AVL

平衡因子 |BF|≤1,四种旋转修复——查询最快、写最贵

红黑树

五条性质把高度按在 2log n 内,旋转 ≤3 次——工程默认

B+树

一节点一磁盘页、扇出 1170、3 层 2000 万行——数据库索引之王

定位:BST 退化是病,三个流派开三张药方——内存里严格平衡(AVL)、内存里松弛平衡(红黑)、磁盘上宽扇出(B+)。前两个管比较树,第三个换赛道用 IO 代价函数重新设计。

The Degeneration Problem

BST 的一切问题:高度失控

裸 BST 的复杂度是 O(h),不是 O(log n)

随机插入时期望 h≈O(log n),但有序输入直接退化成链表:1,2,3,…,n 依次插入,h=n,查找变 O(n)。生产环境不会给你随机数据。

两条修复路线

强制高度差:每步维护 |BF|≤1(AVL)——平衡严格、查询快;② 松弛染色规则:不盯高度,用红黑五性质把高度夹在 2log n 内(红黑)——维护便宜、写快。

第三条路线:换代价函数

磁盘上一次 IO = 10ms 级、一次内存比较 = 纳秒级——树高即 IO 次数。于是放弃二叉、放大扇出到千级:B/B+ 树 3 层装下 2000 万行。

共同不变式

所有平衡树的本质都是同一句话:用插入/删除时的局部调整,换查询时全局的高度上界

结构高度上界调整代价
裸 BSTO(n)(有序输入)
AVL1.44 log n插入 ≤2 旋,删除 O(log n) 旋
红黑树2 log(n+1)插入 ≤2 旋,删除 ≤3 旋
B+ 树(阶≈千)3~4 层(千万行)节点分裂/合并,逐层上传
选型直觉:"读多写少、内存、要严格最快 → AVL;通用库/写多 → 红黑;磁盘/块设备/超大体量 → B+树;纯内存 + 有序 + 简单实现 → 跳表(跳表 deck,Redis zset 的选择)。"
先立问题再讲方案:BST 的 O(h) 与有序输入退化是所有平衡树的出发点。表格给出三族的高度上界与调整代价,是本 deck 的总纲——后面三块各展开一族。

AVL · Four Rotations

AVL:平衡因子与四种旋转

AVL 树的 LL 右旋与 RR 左旋修复示意 左图 LL 型:10 的左孩子 5 的左孩子又插入了 2,左子树过高,对 10 做右旋后 5 成为新根;右图 RR 型:2 的右孩子 5 的右孩子插入 10,对 2 做左旋后 5 成为新根。LR 与 RL 型是对称的双旋转。 LL · 左左失衡 → 右旋 1052 BF=2 右旋 5210 ?? 5 升根,10 带右子树下沉 —— 高度恢复 RR · 右右失衡 → 左旋 2510 左旋 5210 ?? LR / RL:先对儿子旋成 LL/RR,再同上单旋 —— 共四种,两两对称
旋转记忆法:失衡路径的"前两步"定类型——LL 右旋、RR 左旋、LR 先左后右、RL 先右后左。旋转后子树高度回到插入前,所以 AVL 插入修复至多一次双旋、路径向上即止。

AVL · Height Bound & Trade-off

AVL 的保证与代价

平衡因子:BF = 左高 − 右高

每个节点 |BF| ≤ 1。插入沿路径更新高度,第一个失衡点做一次(双)旋转后整棵树恢复——因为旋转把子树高度还原到插入前的值。

最坏高度 1.44 log₂n

最"瘦"的 AVL 树是斐波那契形状:N(h) = N(h−1) + N(h−2) + 1 节点,解得 h ≤ 1.44 log₂n。比 log n 多 44% 常数,换来了"查询路径最短"。

删除为什么贵

删除可能让多个祖先依次失衡,要一路回溯到根,最坏 O(log n) 次旋转——这是 AVL 输给红黑树的核心点。

什么时候选 AVL

查询远多于写、且对查询延迟敏感(如内存索引、语言教学实现)。工程库极少内建 AVL——红黑的"维护便宜"通常更值钱。

指标AVL红黑树
平衡严格度|BF|≤1(严格)高 ≤ 2log(n+1)(松弛)
查询略快(更矮)稍慢
插入旋转≤ 2 次≤ 2 次
删除旋转O(log n) 次≤ 3 次
维护开销存高度/平衡因子1 bit 颜色
库内地位教学/专用品工业默认
一句话总结:"AVL 用更频繁的旋转买最矮的树;红黑树用更松的平衡买更便宜的写。查询密集选 AVL,通用选红黑——大多数库替你选了红黑。"
AVL 三件事:BF 与修复逻辑、1.44 的斐波那契推导(说出 N(h)=N(h-1)+N(h-2)+1 即可)、删除回溯贵。对比表是"AVL vs 红黑"标准答案,重点强调删除旋转次数差异。

Red-Black Tree · Five Invariants

红黑树:五条性质,一条推论

合法红黑树示例与黑高标注 一棵七个节点的合法红黑树:根 13 黑色,孩子 8 和 17 红色,四个孙子 1、11、15、25 黑色;每个红节点的孩子都是黑或空,且任意节点到空叶子的每条路径黑节点数相同。 13817 1111525 nil 黑nil 黑nil 黑nil 黑 根必黑 · 黑高 = 3(含 nil) FIVE PROPERTIES ① 每节点红或黑 ② 根黑 ③ 叶(nil)黑 ④ 红节点孩子必黑(无红红) ⑤ 任意节点到各叶路径黑数相同 推论:最长 ≤ 2 × 最短 最短路径全黑 = bh 最长红黑相间 ≤ 2bh → 高 ≤ 2log(n+1)
五性质要能背,但更要懂第④⑤条如何协作:⑤保证黑高一致,④限制红色不能连续——于是最短路径全黑、最长路径红黑相间,比例被 2:1 卡死。这就是"松弛但可控"的全部数学。

Black Height · Why 2 log n

推论展开:从黑高到 2log(n+1)

三步推导(面试要能口述)

① 性质⑤:任一节点到叶的路径黑数相同,记黑高 bh;
② 最短路径 = 全黑 = bh 长;最长路径红黑相间 = 2bh 长 → 最长 ≤ 2×最短
③ 黑高 bh 的子树至少含 2^bh − 1 个节点(归纳:每层黑数翻倍)→ n ≥ 2^bh − 1 → bh ≤ log₂(n+1) → 高 ≤ 2log₂(n+1)

"近似平衡"的哲学

红黑树不追求最矮(AVL 的 1.44),只追求常数因子内的有界——2 倍以内的路径差,配合"调整便宜",总体赢。

修复逻辑直觉(不必背 case)

插入新节点染(不破坏黑高,只可能违反④红红);红红冲突看叔叔:叔红 → 变色上推;叔黑 → 旋转+变色,一次终结。插入最多 2 次旋转

删除呢

删黑节点会破坏黑高,需要"双黑"修补,情况多但旋转 ≤ 3 次——这是红黑对 AVL 的决定性优势。

// 插入修复核心(伪代码直觉)
for cur != root && cur.parent.red {
    if uncle.red {
        // 叔红:变色,冲突上推两层
        parent.black = uncle.black = true
        grandparent.red = true
        cur = grandparent
    } else {
        // 叔黑:旋转 + 变色,终结
        rotate & recolor
        break
    }
}
root.black = true
面试金句:"红黑树把'平衡'从几何问题(高度差)翻译成了计数问题(黑高一致)——计数好维护:染色是 O(1) 的,旋转才是贵的,红黑的聪明之处在于让大多数修复只用染色解决。"
本页是红黑的理论核心:三步推导必须能口述。右侧伪代码给"叔红上推、叔黑终结"的直觉——面试不要求默写删除 case,但插入逻辑要能讲。金句把"计数 vs 几何"点破。

Red-Black In The Wild

红黑树在哪:从语言库到操作系统内核

语言标准库

Java TreeMap / TreeSetHashMap 的树化桶(链长 ≥8 转 红黑哈希 deck);C++ std::map / set / multimap——有序容器几乎都是红黑。

Linux 内核

CFS 调度器(按 vruntime 排序取最左节点)、epoll(红黑树管理监听 fd)、虚拟内存区域 vma 管理、ext4 的 extent 缓存——内核的"有序集合"默认答案。

中间件

nginx 定时器(红黑树取最近到期)、Java ConcurrentHashMap 1.8 树化桶。共同点:有序 + 高频插入删除 + 最坏情形有界。

Go 呢

标准库没有平衡树(map 走哈希、有序需求交给排序 slice 或第三方)——面试可答"Go 的哲学是组合最小原语",并引到 跳表 这两个"特化替代"。

场景为什么是红黑树
TreeMap / std::map有序遍历 + O(log n) 稳定
HashMap 树化哈希最坏 O(n) → O(log n) 兜底
CFS 调度每次取 vruntime 最小 = 最左节点 O(log n)
epoll fd 管理注册/删除高频 + 按 fd 有序
内核 vma按地址区间有序查找
答题句式:"红黑树统治'内存有序集合'——语言库到内核通用,因为它的最坏情形有界(2log n)且单次修改旋转 ≤3 次,适合不可预测的通用负载。"
应用页的价值在"共同点"提炼:有序 + 高频增删 + 最坏有界。Linux 三个例子(CFS/epoll/vma)是后端面试的加分素材。Go 无内建也是考点——用组合哲学回答。

B+ Tree · Disk-Friendly

B+树:数据全在叶子,叶子手拉手

B+树三层结构与叶子节点链表 B+树根节点存导航键 17 和 35,指向三个叶子节点;叶子分别存放 5 8 12、17 25 28、35 40 46 等真实数据行,叶子之间用双向链表相连支持范围扫描。 <17 17 35 内点=纯导航 5812 172528 354046 叶=数据页(含完整行) 双向链表 双向链表 点查:根 → 叶固定 3 次页访问 · 范围查:定位起点后沿链表顺序扫 B 树对比:数据分布在所有节点(非叶也存行),范围扫描要中序回溯 —— 见下一页对比表
B+树两大结构特征:非叶纯导航(不放数据 → 扇出极大)、叶子链表相连(范围扫描顺顺序 IO)。图上蓝色就是"三次页访问"的固定路径。B 树对比留到下一页表格。

Why B+ Tree Wins On Disk

数据库索引为什么选 B+树:IO 次数 = 树高

维度B+ 树B 树红黑树
数据位置全在叶子,非叶纯导航所有节点都存数据每节点一份数据
扇出≈1170(16KB 页 ÷ 14B 项)较小(内点要放行数据)2
千万行树高3 层(3 次 IO)更高≈23 层 = 23 次 IO
范围查询叶子链表顺序扫中序回溯父节点中序遍历(跳跃指针)
查询稳定性每次到叶,耗时一致可能在非叶命中一致
2000 万怎么算的:16KB 页 / (8B 键 + 6B 指针) ≈ 1170 扇出;3 层 = 1170 × 1170 个叶页;每叶页 16KB ÷ 1KB/行 ≈ 16 行 → 1170² × 16 ≈ 2190 万行。树高即 IO 次数——这就是"矮胖"的全部意义(与 MySQL 索引 deck 同一套账)。

哈希索引为什么不行?

等值查询 O(1) 很香,但不支持范围、排序、最左前缀——WHERE age > 18 在哈希里只能全表扫。InnoDB 有自适应哈希(热点页缓存),但主索引永远是 B+树。

对比表四条主轴:数据位置、扇出、树高、范围查询。"IO 次数 = 树高"是唯一的中心思想——红黑 23 层 vs B+ 3 层,就是 23 次 IO vs 3 次。2000 万的算术要能现场推。

Selection Cheat Sheet

有序结构全家福:一张表选型

结构查找插入/删除范围查询介质与场景
有序数组 + 二分O(log n)O(n)✓ 二分端点静态数据;只读近乎完美
AVLO(log n) 最快常数O(log n) 旋转多✓ 中序内存 · 读多写少
红黑树O(log n)≤3 旋转,最便宜✓ 中序内存 · 通用库/内核默认
跳表O(log n) 期望O(log n),实现极简✓ 底层链表内存 · Redis zset(跳表 deck
仅最值 O(1)O(log n)内存 · 只要最值(堆 deck
B+ 树O(log n) = 3 次 IO分裂/合并✓✓ 链表顺序扫磁盘 · 数据库索引
LSM 树O(log n)~多路顺序写 O(1) 摊✓ 需归并磁盘 · 写多读少(RocksDB)
选型三问:① 数据在哪(内存 / 磁盘)?② 读写比?③ 要不要范围?——三问答完,这张表自动给出唯一答案。
选型总表是本 deck 的收官输出:七种有序结构、三个决策维度。LSM 一行是加餐——写多的磁盘场景 B+ 也有对手,能提一句 RocksDB 就是加分。

Interview QA · Part 1

高频追问:AVL 与红黑树

1 · AVL 的平衡因子?四种旋转?

LL 右旋LR 双旋

BF = 左高 − 右高,|BF| ≤ 1。失衡路径前两步定型:LL 右旋、RR 左旋、LR 先左旋儿子再右旋、RL 相反。旋转把子树高度还原到插入前,修复即止。

2 · AVL 最坏高度为什么是 1.44 log n?

斐波那契树

最少节点的 h 层 AVL 树满足 N(h) = N(h−1) + N(h−2) + 1(两子树一高一矮)——斐波那契增长,解出 h ≤ 1.44 log₂n。最坏形状即"斐波那契树"。

3 · 红黑树五性质?哪条最关键?

④⑤ 协作

红黑根黑nil黑、无红红、黑高一致。最关键是 ④+⑤ 的组合:⑤钉死黑高一致,④限制红不连续——合起来推出"最长 ≤ 2×最短"。

4 · 为什么红黑高度 ≤ 2log(n+1)?

黑高计数

最短路径全黑 = bh;最长红黑相间 ≤ 2bh;黑高 bh 的子树至少 2^bh−1 个节点 → bh ≤ log₂(n+1) → 高 ≤ 2log₂(n+1)。三步口述即可。

5 · 红黑 vs AVL 怎么选?

旋转次数

AVL 更矮、查询略快,但删除要回溯 O(log n) 次旋转;红黑高度放宽到 2log n,但插入 ≤2、删除 ≤3 次旋转,修改便宜。读极多选 AVL,通用/写多选红黑。

6 · 红黑插入为什么最多 2 次旋转?

叔红上推叔黑终结

新节点染红(不破坏黑高);若父也红:叔红 → 变色把冲突上推两层(无旋转);叔黑 → 一次旋转+变色终结。上推要么到根(0 旋转),要么中途遇黑叔(≤2 旋转)。

7 · Java HashMap 树化为什么用红黑不用 AVL?

退化兜底

树化是哈希退化的兜底,负载不可预测、可能反复增删——要的是"任何序列下修改便宜 + 最坏有界",正是红黑的强项;AVL 的删除回溯在退化场景反而危险。

8 · Linux 内核哪里用红黑树?

CFS / epoll / vma

CFS 调度器按 vruntime 排序(取最左 = 下一个运行进程);epoll 用红黑树管理注册 fd(增删高频);进程地址空间 vma 按地址有序。共同点:有序 + 高频增删 + 内核不能容忍最坏退化。

前八题把 AVL 与红黑的核心全部覆盖:旋转四型、1.44 推导、五性质、2log 推导、选型、插入修复、HashMap 树化、内核应用。第 6 题的"染色便宜、旋转贵"是理解红黑设计的钥匙。

Interview QA · Part 2

高频追问:B+树与有序结构选型

9 · B 树和 B+ 树的区别?

数据位置叶子链表

B 树数据分布在所有节点,可能在非叶提前命中;B+ 树数据全在叶子且叶子链表相连——非叶纯导航扇出更大、范围查询顺序扫、每次查询稳定到叶。

10 · 为什么 MySQL 索引用 B+ 树不用红黑树?

IO 次数=树高

二叉扇出 2,2000 万行高 ≈23 层 = 23 次磁盘 IO(每次 ~10ms 级);B+ 树 16KB 页做节点、扇出 ≈1170,3 层搞定。数据在磁盘上,"矮胖"就是生命。

11 · 为什么不用哈希索引?

无范围

哈希等值 O(1) 但不支持范围扫描、排序、最左前缀匹配——BETWEEN / ORDER BY / LIKE 'x%' 全废。InnoDB 自适应哈希只是热点页的旁路缓存。

12 · B+ 树叶子链表具体带来什么?

顺序 IO

范围查询定位到起点叶后,沿链表顺序读下一批叶页——磁盘预读友好,几乎无随机 IO;删改时链表维护 O(1)。这是"范围查询之王"的机制来源。

13 · B+ 树的分裂与合并?

自底向上

插入时叶页满 → 从中间分裂、分裂键上提到父,父满继续上传(可能长高一层——唯一长高方式);删除后页利用率低于阈值则与兄弟合并/重分布,父层收缩。

14 · 跳表凭什么和平衡树竞争?

实现简单范围天然

期望 O(log n) 与红黑同档,但插入删除只改指针、无旋转无染色,实现行数少一个量级;底层链表做范围查询同样顺。Redis zset 的选择(跳表 deck)。

15 · LSM 树了解吗?和B+ 树怎么分?

写多读少

LSM 把随机写变顺序写(memtable + 分层 SSTable + 后台 compaction),写吞吐远超 B+;代价是读要查多层、compaction 有写放大。RocksDB/LevelDB 用 LSM,MySQL 用 B+——读写比定生死。

后七题:B/B+ 对比、磁盘选型三连(红黑/哈希/LSM)、叶子链表机制、分裂合并。第 10 题的算术(1170 扇出 × 3 层 ≈ 2000 万)与 MySQL deck 数字一致,两边可互为印证。

Related & References

相关知识点与参考

数据结构系列(本分类)

二叉树 →(BST 基础与退化问题)
跳表 →(有序结构的极简替代)
堆与优先队列 →(只要最值的特化)
哈希表 →(树化的来源:Java HashMap)

MySQL / Redis / 算法系列

MySQL B+ 树索引 →(页/扇出/聚簇落地细节)
Redis 数据结构 →(zset 为何选跳表)
二分查找 →(有序结构的通用检索)
复杂度分析 →(log 下界的来源)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch13 · Red-Black Trees五性质、黑高引理(高 ≤ 2log(n+1))、插入删除修复
Adelson-Velsky & Landis (1962)AVL 原始论文;高度界 1.44 log n 的斐波那契论证
Linux kernel lib/rbtree.c · kernel/sched/fair.c内核红黑树实现与 CFS 调度器应用
MySQL 8.0 Manual · InnoDB Index Structures16KB 页、B+ 树组织;对照本仓库 index-btree deck
Bayer & McCreight (1972) · B-treesB 树原始论文(B+ 为其数据库化变体)
收尾:本 deck 是"结构层",MySQL index-btree deck 是"InnoDB 落地层",跳表/堆是特化替代——四边互链。出处两条原始论文(AVL 1962、Bayer 1972)+ CLRS + 内核源码。总页数 13。