深色模式
00 · 数据结构导读:数据怎么摆,决定了它有多快
数据结构不是"八种容器的名字",而是**"用空间换时间"的各种具体交易**。看懂这句话,这一模块就不用背了。
一句话本质
所有数据结构都在做同一件事:用额外的空间或额外的约束,换取某种操作更快。
- 数组:浪费"插入要挪动"的代价,换取"按下标随机访问 O(1)"
- 哈希表:浪费空间(装不满),换取"查找 O(1)"
- 树:浪费指针,换取"有序 + 查找 O(log n)"
- 堆:只维护"局部有序",换取"取最值 O(1)"
没有免费的午餐,只有不同的交易。选结构 = 选你要优化哪个操作、愿意付什么代价。
生活类比:找东西的不同方式
| 结构 | 场景 |
|---|---|
| 数组 | 电影院的连排座位。知道排号就能直接坐下(O(1)),但中间要插一个人,后面全得挪 |
| 链表 | 寻宝游戏:每张纸条写着下一张在哪。插入方便,但要找第 10 张必须从第 1 张开始 |
| 栈 | 一摞盘子。只能从最上面拿、往最上面放 |
| 队列 | 排队买奶茶。先来的先走 |
| 哈希表 | 超市存包柜。按号码直接开柜,不用一个个找 |
| 二叉搜索树 | 公司组织架构。找一个人,从 CEO 开始"大了往左、小了往右" |
| 堆 | 急诊分诊。不要求全员有序,只要求"最严重的那个永远在最前面" |
| 图 | 地铁线路图。站点之间有关系,找最短路径 |
模块地图:5 篇怎么接
01 复杂度 —— 怎么衡量"快"(全模块的度量尺)
|
+-- 02 线性结构:数组 / 链表 / 栈 / 队列 / 哈希表
| 优化"按下标访问"或"插入删除"
|
+-- 03 树与堆:二叉树 / 平衡树 / 堆
| 优化"有序查找"和"取最值"
|
+-- 04 图与并查集:最短路 / 拓扑排序 / 连通性
| 处理"多对多关系"
|
+-- 05 排序与二分:把前面所有结构用起来
排序是"预处理",二分是"利用有序"关键连接:
- 01 是尺子。后面所有"这个结构更快"的结论,都要用 01 的语言(O(n)、O(log n))表述。
- 02 的哈希表 和 03 的树 是"查找"的两种方案:哈希 O(1) 但无序,树 O(log n) 但有序。这个对比直接决定了数据库索引为什么用树不用哈希。
- 03 的树 是 04 数据库 B+ 树的前置。不先懂二叉树和平衡树,B+ 树就是死记硬背。
- 05 的排序 是所有算法的"预处理步骤",而二分是"利用有序"的极致。
一条主线:为什么数据库用 B+ 树而不用哈希
这条线把整个模块串起来,建议先看一遍:
问题:1000 万行数据,怎么快速找到某一行?
方案 1:数组 + 线性扫描
> O(n),1000 万次比较,太慢
方案 2:哈希表
> 查找 O(1),很快
> 但:① 不支持范围查询(age > 20 得全扫)
② 不支持排序(ORDER BY 用不上)
③ 哈希冲突退化,且扩容代价大
> 结论:等值查询可以,范围查询不行
方案 3:二叉搜索树
> 查找 O(log n),且天然有序
> 但:① 退化成链表时是 O(n)(所以要平衡树)
② 每个节点只存一个值 > 树高 log2(1000万) ≈ 24 层
③ 每层一次磁盘 IO > 24 次 IO,太慢
方案 4:B 树(多叉)
> 每个节点存几百个值 > 树高降到 3~4 层
> 磁盘 IO 从 24 次降到 3~4 次
方案 5:B+ 树(B 树的改良)
> 数据只在叶子节点,非叶子只存索引 > 一个节点能装更多键 > 树高更低
> 叶子节点用链表串起来 > 范围查询直接顺序扫,不用回到根
> 结论:这是数据库的最终选择看懂这条线,你就同时懂了:数组、哈希、二叉树、平衡树、B 树、B+ 树,以及它们各自的取舍。这比背六段定义有用得多。
学习这个模块的方法
- 每个结构都问三个问题:它优化了哪个操作?代价是什么?什么场景下会退化?
- 一定要手写。二分和快排的边界,不手写三遍永远记不住。
- 不要背代码,背"为什么这么写"。比如二分为什么用
left + (right-left)/2(防溢出),为什么循环条件是left <= right(闭区间)。 - 复杂度要能"看出来",不是背出来的。看到"每次砍一半"就是 log n,看到"嵌套两层"就是 n²。
和其他模块的连接
- < 来自 虚拟内存:让每个程序都以为自己独占一台机器:数组快于链表的根本原因是缓存局部性,属于内存布局问题
- > 影响 索引与 B+ 树:B+ 树是这里树结构的直接应用,一个节点 = 一个磁盘页(16KB)
- > 影响 事务与 ACID:undo log 是链表、redo log 是环形数组,都是这里的基础结构
- > 影响 工程实践:Redis 的跳表(有序链表 + 多级索引)、Java 的 HashMap(数组 + 链表 + 红黑树)、Kafka 的稀疏索引
自测 3 问
Q1:为什么说"所有数据结构都是空间换时间"?这个说法有反例吗?
点开看答案
大体成立,但不绝对。更准确的说法是"用一种操作的代价,换另一种操作的效率"。
- 哈希表:确实是用空间(装不满的桶)换查找时间。
- 链表:反而是用时间换空间(插入快,但查找慢,且每个节点多一个指针的空间开销)。
- 数组:用"插入慢"换"随机访问快",空间上还更省(没有指针)。
- 堆:用"整体无序"换"取最值快"。
所以真正的统一表述是:没有一种结构在所有操作上都最优,选结构就是选"哪个操作最重要"。面试时这样答,比背"空间换时间"更显得懂。
Q2:既然哈希表查找 O(1),为什么不用它做数据库索引?
点开看答案
三个致命问题:
- 不支持范围查询。
WHERE age > 20用哈希只能全表扫,因为哈希把相邻的值打散了。B+ 树叶子有序且相连,范围查询直接顺序扫。 - 不支持排序。
ORDER BY用不上哈希索引,只能额外排序。 - 最左前缀失效。联合索引
(a,b),只查b时哈希完全用不了;B+ 树可以利用前缀。
哈希索引确实存在(MySQL 的 MEMORY 引擎、InnoDB 的自适应哈希索引),但只用于等值查询的加速,是补充而不是主力。
Q3:为什么平衡树能保证 O(log n),普通二叉搜索树不能?
点开看答案
因为普通 BST 的形态取决于插入顺序。
按 1,2,3,4,5 顺序插入,树会退化成一条链(每个节点只有右孩子),查找变成 O(n)——和链表没区别。
平衡树(AVL、红黑树)通过旋转维持"左右子树高度差不超过常数",强制树高保持在 O(log n),所以最坏情况也是 O(log n)。
代价:每次插入/删除可能触发旋转,常数开销变大,实现也复杂得多。这就是"用一点性能换最坏情况的保证"。
类比失效处 / 易错点
- "电影院座位"类比会让人以为数组插入一定慢。在尾部追加是 O(1)(摊销),只有在中间插入才要挪动。
- "哈希 O(1)"是平均情况。最坏情况(全部冲突)是 O(n),Java 8 之后冲突多了会转红黑树变成 O(log n)。
- "树都是 O(log n)"不成立。只有平衡的树才是;普通 BST 最坏 O(n)。
- 复杂度忽略常数,但工程里常数很重要。O(log n) 的树在 n 很小时,可能比 O(n) 的数组还慢(缓存不友好 + 指针跳转)。
- "堆是有序的"是错的。堆只保证"根是最值",其他位置无序。所以堆不能做范围查询,也不能中序遍历出有序序列。
✅ 验收清单
- 能用一句话说清"选数据结构本质是在权衡什么",并举出数组/链表/哈希/堆各自优化了哪个操作
- 能画出模块地图,说出 5 篇之间的依赖关系(尤其"树 > B+ 树"这条)
- 能完整讲出"为什么数据库用 B+ 树"那条主线(从数组 > 哈希 > BST > B 树 > B+ 树)
- 能说出哈希表不适合做数据库索引的三个原因
- 能解释"普通 BST 会退化成链表,平衡树怎么避免"
- 合上书,用自己的话讲 3 分钟,中途不卡壳