[数据结构/MYSQL] 二叉搜索树(BST) VS 平衡二叉排序树(AVL) VS B树(平衡多路搜索树) VS B+树 VS 红黑树(平衡二叉B树)

1 二叉排序树/二叉查找树/Binary Sort Tree

  • 1种对排序和查找都很有用的特殊二叉树
  • 叉排序树的弊端的解决方案:平衡二叉树
  • 二叉排序树必须满足的3条性质(或是具有如下特征的二叉树)
    • 若它的左子树不为空,则:左子树上所有结点的值< 它根结点的值
    • 若它的右子树不为空,则:右子树上所有结点的值 > 它根结点的值
    • 它的左子树、右子树也分别为二叉排序树(递归性)

(按照如上定义,即:
1 无键值相等的结点
2 中序遍历一颗二叉树时,可得一个结点值递增的有序序列)

2 平衡二叉排序树/Balanced Binary Tree/Adelson-Velskii & Landis

  • 1种结构平衡的二叉搜索树(即 叶节点高度差的绝对值不超过1)
  • 平衡因子:AVL树中左子树与右子树的深度之差只能为-1、0、1
  • AVL树的平衡调整方法:LL、RR、LR、RL(为了保证平衡,需付出一定代价)
  • 平衡二叉树的3条性质:(AVL树or空树,或是具有如下特征的二叉排序树(BST))
    • 左子树必须为平衡二叉树
    • 右子树必须为平衡二叉树
    • |左子树深度-右子树深度|≤1

3 B树

  • 1种平衡的多路搜索树,多用于文件系统、数据库的实现
  • 缺点
    • 在查询单条数据是非常快的。但如果范围查的话,b树每次都要从根节点查询一遍。
  • 提出者
    • 最早是由德国计算机科学家Rudolf Bayer等人于1972年在论文 《Organization and Maintenance of Large Ordered Indexes》提出的
  • 结点的结构图↓
  • 1棵m阶的B树,或为空树,或为满足下列特性的m叉树:
    • 若根结点不是叶子结点,则:有≥2棵子树
    • 树中每个结点有≤m棵子树 (m叉树的性质)
    • 所有的非终端结点有≤m-1个关键字。
    • 除根结点外的所有非终端结点有≥「m/2」棵子树
    • 所有的叶子结点都出现在同一层次上,并且不带信息(即 失败结点)

(实质上,失败结点并不存在,指向此类结点的指针为空,引入失败结点是为便于分析B树的查找性能)

4 B+树

  • 1种B树的变形树,更适合用于文件索引系统。
  • 实际应用:MySQL的存储引擎InnoDB/MyISAM均利用B+树建立索引。(MYSQL-Console:SHOW INDEX FROM tableName)
  • m阶B+树与m阶B树的差异
    • 含有n个关键字的结点必含有n棵子树;
    • 所有的【非叶子节点/非终端结点】:可看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字
    • 所有的【叶子结点/终端节点】:包含了全部关键字的信息,以及指向含这些关键字记录的指针,且叶子结点本身依靠关键字的大小自小而大顺序连接;

5 红黑树/平衡二叉B树

  • 别称:平衡二叉B树,1种自平衡的二叉搜索树
  • 实际应用:HashMap等Java的JDK源码实现
  • 红黑树必须满足的5条性质
    • 节点必为Red or Black
    • 根节点必为Black
    • 叶子节点必为Black
    • Red节点的子节点(父节点)必为Black (即 从根节点到叶子节点的所有路径上不能有2个连续的Red节点)
    • 从任一节点到叶子节点的所有路径都包含相同数目的Black节点
  • 比起AVL树(平衡二叉树),红黑树的特点↓

F 附件

F.1 二叉树的遍历方法:前序遍历、中序遍历、后序遍历

  • 二叉树三种遍历(递归定义,根节点访问顺序区分)

记口诀:前根左右,中左根右,后左右根

1. 前序遍历(Pre-order)

根 → 左子树 → 右子树

  1. 访问当前根节点
  2. 递归遍历左子树
  3. 递归遍历右子树

2. 中序遍历(In-order)

左子树 → 根 → 右子树

  1. 递归遍历左子树
  2. 访问当前根节点
  3. 递归遍历右子树

✅ 二叉搜索树:中序遍历结果是升序序列

3. 后序遍历(Post-order)

左子树 → 右子树 → 根

  1. 递归遍历左子树
  2. 递归遍历右子树
  3. 访问当前根节点

✅ 适合删除树、计算子树信息(要先处理完所有子节点)

重要意义:还原二叉树

  • 前序 + 中序,可以唯一还原二叉树
  • 后序 + 中序,可以唯一还原二叉树
  • 只有前序+后序,不能唯一还原(缺少中序,无法区分左右)

简单示例

    A
   / \
  B   C
 /
D
  • 前序:A → B → D → C
  • 中序:D → B → A → C
  • 后序:D → B → C → A

Python 递归模板

# 前序
def pre(root):
    if not root: return
    print(root.val)
    pre(root.left)
    pre(root.right)

# 中序
def ino(root):
    if not root: return
    ino(root.left)
    print(root.val)
    ino(root.right)

# 后序
def post(root):
    if not root: return
    post(root.left)
    post(root.right)
    print(root.val)

非递归(栈)版本: 略

像MYSQL等数据库在做数据恢复时,有使用上这些遍历方法吗?

  • 结论

二叉树的前/中/后序遍历本身不会直接用于 MySQL 数据恢复,但 MySQL 底层索引(B+树)的遍历逻辑和这几个遍历思想高度相关;真正做数据恢复靠的是 binlog、redo log、undo log。

1. B+树 和 二叉树遍历的区别

MySQL InnoDB 的主键索引是 B+树(多路平衡查找树,不是二叉树)

  • B+树的特点:所有数据都在叶子节点,叶子节点是【双向链表】串联
  • 查询、范围扫描:顺着叶子节点链表顺序遍历,等价于【中序遍历】效果(节点值:从小到大)

中序遍历二叉搜索树得到有序序列,B+树叶子链表扫描也是有序,这是【思想同源】。
但是:B+树没有前序、后序遍历的业务场景,B+树几乎只用【叶子节点】做顺序扫描。

2. MySQL 数据恢复到底用什么日志(重点)

redo log(重做日志)

崩溃恢复核心。物理日志,记录「页修改」
宕机重启时:扫描 redo log,把【已提交但没刷到磁盘的数据页】重做,保证数据不丢。
恢复逻辑:按日志产生的时间顺序回放,和树遍历无关。

binlog(二进制日志)

逻辑日志,记录 DML/DDL,用于时间点恢复 PITR、主从复制
数据误删恢复:全量备份 + 按顺序回放 binlog,恢复到指定时间点。
回放:按事务先后顺序执行,和树遍历无关。

undo log(回滚日志)

事务回滚、MVCC,保存数据修改前的旧版本。
rollback 的时候,读取 undo log 把数据改回去。

3. 什么时候会涉及“树遍历”在数据库恢复场景?

只有一种场景:索引损坏,需要重建索引
ALTER TABLE xxx FORCE; 或者重建索引:
引擎会扫描【聚簇索引叶子节点】(B+树顺序遍历),读取全部行,重新构建【二级索引B+树】。

👉 这里的遍历,只是B+树叶子节点顺序扫描,不是二叉树前/中/后序。

4. 重点区分

  1. ❌ MySQL 数据恢复使用二叉树前/中/后序遍历 → 错误
  2. ✅ InnoDB B+树范围查询,类似二叉搜索树中序遍历得到有序结果 → 正确
  3. ✅ 数据误删恢复靠 binlog;宕机崩溃恢复靠 redo log;事务回滚靠 undo log

5. 类比总结

  • 二叉树三种遍历:是二叉树的节点访问顺序理论
  • B+树叶子链表顺序扫描:是多路树的有序读取,思想借鉴中序,但不是同一个东西
  • 数据恢复:是日志回放机制,和树遍历没有直接关系

F.2 B 树 / B + 树 / 二叉搜索树

遍历对比

结构 树类型 数据存放位置 常用遍历方式 遍历效果 备注
二叉搜索树 BST 二叉树 每个节点都存数据 中序遍历(左→根→右) 升序有序序列 前/后序很少用于查询;容易退化成链表
B树(多路平衡查找树) 多路树 所有节点(根、中间、叶子)都存索引+数据 中序式遍历:从最左子树开始,依次访问节点内有序key,再访问右子树 升序有序 没有专门“前/后序”业务场景;查找时自上而下遍历分支
B+树(InnoDB索引) 多路树 【叶子节点】存完整数据;【非叶子】只存索引(关键字/key) 1.单点查找:自上而下从根找到叶子
2.范围查询:叶子节点双向链表顺序扫描
升序有序,范围扫描极快 不会像BST那样递归中序遍历;直接走叶子链表,是MySQL索引核心

小结:BST的中序遍历是递归遍历树节点;B/B+树范围查询虽然结果也是有序,但B+树靠叶子链表,不是递归遍历整棵树。

特点综合对比

对比项 二叉搜索树(BST) B树(多路平衡查找树) B+树
节点子节点数 最多2个(左、右) \([⌈m/2⌉, m]\),根节点最少2个 内部节点:\([⌈m/2⌉, m]\);叶子节点无孩子
关键字分布 所有节点都存数据+关键字 所有节点都存关键字+对应数据 内部节点只存索引关键字,叶子节点存全部数据
查找路径 最坏O(n)(退化成链表);平衡BST为O(logn) 从根到叶子,单次IO链,O(logn) 一定走到叶子节点;范围查询优势大
范围查询 需要中序遍历,效率低 需要来回跨节点,一般 叶子节点链表串联,顺序扫描,范围查询最优
磁盘场景 不适合磁盘,树太高IO多 适合磁盘,高度低;随机查询不错 数据库、索引首选,IO次数稳定,范围查询强
叶子节点关系 无关联 叶子节点不相连 叶子节点构成有序双向/单向链表
数据重复 一般不允许重复key B树节点内关键字唯一 叶子可存重复索引
典型应用 内存查找、集合(平衡BST:红黑树) 文件系统索引(部分) MySQL InnoDB索引
  1. BST:二叉,树高容易失控多用于内存
  2. B树:多路平衡,每个节点都带数据,随机查询好,范围查询一般
  3. B+树:B树变种,数据全在叶子,叶子链表串联,范围查询极强(关系型)数据库索引标准

优缺点、适用场景对比

  • BST、B树、B+树:优缺点、适用场景对比
对比项 二叉搜索树(BST) B树(多路平衡查找树) B+树
优点 1. 实现简单 2. 中序遍历直接得到有序序列 3. 平衡BST(红黑树、AVL)内存查找性能优秀 1. 多路平衡,树高度低,磁盘IO少 2. 查找键命中非叶子节点即可返回数据,随机查询快 3. 插入删除相对均衡 1. 所有查询最终落到叶子,IO次数稳定 2. 叶子节点链表相连,范围查询极强 3. 非叶子节点仅存索引,一页能放更多关键字,树更矮 4. 数据都在叶子,数据库分页扫描友好
缺点 1. 普通BST在顺序插入时会退化成链表,查找退化到O(n) 2. 树高随数据量增长快,磁盘场景IO次数多,不适合磁盘存储 3. 范围查询需要中序遍历,效率差 1. 范围查询需要多次跨节点跳转,效率弱于B+树 2. 每个节点都存数据,单页存放关键字数量变少,树相对更高 3. 删除非叶子节点比较复杂 1. 随机查询通常要走到叶子节点,比B树多一次IO 2. 插入删除需要维护叶子链表,逻辑略复杂
适合场景 内存内的有序查找;平衡BST(红黑树)用于TreeMap、C++ set/map等内存容器 文件系统索引(NTFS等);部分数据库的老版本索引 MySQL InnoDB索引;绝大多数关系型数据库索引,大数据【范围查询】场景

补充记忆要点:

  • BST:内存用,最怕数据有序插入失衡;
  • B树:随机查找强,范围查询弱;
  • B+树:范围查询王者,数据库首选。

F.3 哈希表、红黑树、B树、B+树

数据结构 优点 缺点(为什么不用)
哈希表 等值查询 O(1) 不支持范围查询,无法排序;有哈希冲突
红黑树 内存中平衡性好
(多用于:内存容器,如: HashMap/TreeMap)
二叉结构树高过大(千万级数据深度 20+),磁盘 IO 次数多
B 树 多路平衡,树矮
(多用于:文件系统,如:NTFS)
非叶子节点也存数据,单页可容纳的指针更少,树更高;叶子节点无链表,范围查询需中序遍历
B+ 树 树矮、IO 少、范围查询快、性能稳定
(多用于:关系型数据库,如:MYSQL/PG)
非叶子节点不存数据(对「只查非索引列」的场景无优化)

核心结论:B+ 树以「非叶子节点纯索引化 + 叶子节点链表化」两个设计,同时解决了树高(IO 次数)范围查询(顺序扫描)两大问题,是最适配磁盘存储的索引结构。这是 MySQL 索引题最高频的深挖点,务必能画出结构图并讲清对比。

F.4 大数据组件是否使用 BST / B 树 / B + 树?

  • 推荐文献
  • LSM-Tree ≠ B/B + 树;LSM 是日志合并树,RocksDB 底层是 LSM,不是 B + 树;OLAP 库(Doris/ClickHouse)一般不用 B + 树做主索引,用稀疏索引。

Doris / Clickhouse / OpenGemini / InfluxDB / Redis / RocksDB

系统 是否用BST 是否用B树 是否用B+树 底层核心索引结构 补充说明
Apache Doris ❌ 不做主索引 稀疏前缀索引、ZoneMap、BloomFilter、倒排索引、BKD-Tree OLAP列式存储,没有B+树主键索引;BKD树用于多维/数值范围查询,不是B+树
ClickHouse ❌ 不做主索引 MergeTree稀疏主键索引、minmax跳数索引、BloomFilter 稀疏索引(每8192行存一条标记),不是B+树,块粒度定位,不是行级索引
OpenGemini(时序库) 倒排索引,底层KV存储复用LSM-Tree 时序库,标签走倒排;底层存储层是LSM,不使用B+树做主索引,B+树随机写差不适合时序高吞吐写入
InfluxDB TSI倒排索引 + TSM文件(块内二分查找) TSI是LSM风格时序倒排索引;TSM内部块索引只是有序数组二分查找,不是B+树
Redis ✅(少量场景) 哈希表(主)、跳表(zset) Redis zset用跳表,不是BST;仅内部少量辅助结构有用二叉树;RDB/AOF持久化本身不使用B/B+树。 ⚠️ Redis on Flash(RocksDB后端)才走LSM
RocksDB LSM-Tree(MemTable默认跳表 + SSTable) SSTable内部块索引是有序数组+二分查找,不是B+树;很多人容易把LSM和B+树搞混
  1. B+树:MySQL InnoDB 行存OLTP首选;上面这5个都没有拿B+树做主索引
  2. LSM-Tree(RocksDB、OpenGemini底层KV、InfluxDB TSI):适合高吞吐写入,牺牲部分读性能,和B+树是两大路线
  3. Doris/ClickHouse OLAP:稀疏索引 + 块过滤(minmax/zoneMap/BloomFilter),按数据块扫描,不是单行索引
  4. Redis:核心是哈希、跳表,不用B/B+树
  • 补充:容易踩坑知识点
  • ❌ 误区:RocksDB = B+树。。RocksDB是LSM;B+树是原地更新,大量随机IO;LSM顺序写,后台compaction。
  • ❌ 误区:时序库用B+树。,时序写入压力极大,B+树随机写会崩,普遍用LSM/倒排。
  • ❌ 误区:ClickHouse主键=B+树。,是稀疏标记索引,定位数据块,块内仍然扫描。

B+树 vs LSM-Tree 的对比表

  • B+树 vs LSM-Tree 对比表
对比项 B+树 LSM-Tree(日志合并树)
核心思想 原地更新,在B+树上直接修改节点;数据全部在叶子有序链表 不原地修改,新写入先写内存MemTable;满了落盘成有序SSTable;后台异步合并SST
写入方式 随机写。更新/插入可能修改磁盘上旧页,产生随机IO 顺序写为主。写入只追加,极少原地修改,写放大取决于compaction策略
读路径 一次查找,从根走到叶子;IO次数稳定 命中页缓存时读性能极好 先查MemTable,再逐层查多层SST;可能多次磁盘IO;存在读放大
写放大 ,修改只影响少量页面 。compaction需要读取、重写旧SST文件,产生写放大
读放大 ,一次查询少量IO 较大,查询需要遍历多个SSTable,要判断数据版本/墓碑
空间放大 较小,页内填充率可控制 较大,旧版本数据、墓碑标记会占用空间,直到compaction清理
更新/删除 原地覆盖;删除标记或直接移除记录 追加写入墓碑(tombstone),不立即删旧数据;后台compaction才清理
随机读 ⭐⭐⭐⭐⭐ 优秀(OLTP单行查询) ⭐⭐⭐ 一般,多层SST查找开销高
批量范围扫描 ⭐⭐⭐⭐⭐ 优秀,叶子链表连续有序 ⭐⭐⭐⭐ 较好,但跨SST合并读取,略弱于B+树
高吞吐写入 ⭐⭐ 差,大量随机IO瓶颈 ⭐⭐⭐⭐⭐ 强,适合高并发大量写入场景
磁盘IO特征 大量随机IO 以顺序IO为主,compaction阶段会有大量读写
典型数据库/组件 MySQL InnoDB、PostgreSQL(B+变种) RocksDB、LevelDB、OpenGemini底层KV、InfluxDB、TiKV
适用场景 OLTP业务:单行CRUD、事务、低延迟随机查询 高吞吐写入场景:时序数据库、分布式KV、大数据存储引擎
  • 关键要点
  1. B+树:原地更新,随机读强,随机写弱。更新会修改磁盘旧页,写压力大,适合读多写少OLTP。
  2. LSM-Tree:追加写,写强随机读弱。牺牲读、空间换取超高写入吞吐量;三大放大:写放大、读放大、空间放大
  3. 核心取舍:B+树把代价放在写入时;LSM把代价放到后台compaction

补充:RocksDB可以配置压缩、compaction策略来降低放大,但无法消除LSM天然存在的三大放大问题。

X 参考文献

  • 参考
  • 推荐:
posted @ 2020-03-15 12:22  千千寰宇  阅读(521)  评论(0)    收藏  举报