编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • 数据结构与算法专栏
  • 基础认知

  • 线性结构

  • 树与哈希

  • 工业级实现

  • 算法思想

  • 实战与综合

    • 实战与综合
    • 算法工程化案例
    • 思考题和问答分析
      • 目录指引与导读
        • 二级目录
      • 01. 从一个工作案例说起
        • 1.1 面试现场:候选人的经典翻车
        • 1.2 问题不在"记不住",而在"没想通"
        • 1.3 本篇的打开方式
      • 02. 内功题时空复杂度
        • 2.1 代表题:以下代码的复杂度是多少?
        • 2.2 解题步骤
        • 2.3 答案要点
        • 2.4 延伸:均摊复杂度
      • 03. 设计题扰动函数
        • 3.1 代表题:为什么要 h ^ (h >>> 16)?
        • 3.2 解题步骤
        • 3.3 答案要点
        • 3.4 进阶:如果 n 不是 2 的幂呢?
      • 04. 并发题原子复合
        • 4.1 代表题:下面代码为什么并发不安全?
        • 4.2 解题步骤
        • 4.3 答案要点
        • 4.4 进阶:computeIfAbsent 能在 lambda 里递归调用自己吗?
      • 05. 内存题LinkedList代价
        • 5.1 代表题:100 万个 Integer 在 ArrayList 和 LinkedList 中各占多少内存?
        • 5.2 解题步骤
        • 5.3 答案要点
      • 06. 设计题LRU五行实现
        • 6.1 代表题:为什么 removeEldestEntry 能让 LRU 代码优雅到 5 行?
        • 6.2 答题框架
        • 6.3 答案要点
      • 07. 跨语言Go与Java
        • 7.1 代表题:Go 标准库没有 Set / LinkedList / TreeMap,为什么?
        • 7.2 答题要点
        • 7.3 两种哲学的取舍
      • 08. 容量题HashMap初始化
        • 8.1 代表题:要存 10000 个元素,HashMap 初始容量填多少?
        • 8.2 公式推导
        • 8.3 一行优雅写法
      • 09. 架构题百亿URL去重
        • 9.1 代表题:URL 去重单机 HashSet 放不下(~480GB),怎么办?
        • 9.2 三层方案
        • 9.3 参数设计
        • 9.4 答案要点
      • 10. 本篇收获案例回扣
      • 11. 思考题给你自己命题
      • 12. 课后作业实战
        • 作业 1:建立你的"错题本"
        • 作业 2:画 20 张"让你忘不掉"的图
        • 作业 3:面试 / 被面试
        • 作业 4:LeetCode 终极刷题清单(Top 50)
      • 13. 进阶专题与延伸
        • 13.1 一张图看懂整个系列
        • 13.2 数据结构的"前沿"——读完系列后还能学什么
        • 13.3 操作系统里的数据结构——Linux 内核的"私藏"
        • 13.4 CS 经典教材推荐(按难度递增)
        • 13.5 online judge 平台推荐与刷题策略
        • 13.6 企业工程实践的"数据结构选型"方法论
        • 13.7 如何在面试中"展示数据结构功底"
        • 13.8 将这套知识用到业务上:四个落地方向
        • 13.9 结语:数据结构是工程师的"元技能"
  • 算法题考核

  • 算法
  • 实战与综合
杨充
2021-09-12
目录

思考题和问答分析

# 20.思考题和问答分析

本篇是数据结构算法专栏的收官总结,汇总前 19 篇的代表性思考题,给出结构化解题范式和参考答案要点。不是"标准答案大全",而是"面试/面壁时该怎么想"的思维框架。

# 目录指引与导读

阅读建议:每节先看 "代表题",自己尝试 5 分钟再看 "解题步骤";最后才看 "答案要点"。20 篇全套思考题,建议建错题本逐一手写。

# 二级目录

  • 01. 从一个工作案例说起
  • 02. 内功题时空复杂度
  • 03. 设计题扰动函数
  • 04. 并发题原子复合
  • 05. 内存题LinkedList代价
  • 06. 设计题LRU五行实现
  • 07. 跨语言Go与Java
  • 08. 容量题HashMap初始化
  • 09. 架构题百亿URL去重
  • 10. 本篇收获案例回扣
  • 11. 思考题给你自己命题
  • 12. 课后作业实战
  • 13. 进阶专题与延伸

# 01. 从一个工作案例说起

# 1.1 面试现场:候选人的经典翻车

一位 5 年工龄的候选人面试 P6 岗位,面试官问:

"HashMap 的 put 扩容时,为什么新桶位置要么是原位置、要么是原位置 + oldCap?"

候选人回答:

"因为 HashMap 的容量是 2 的幂次方……然后……扩容的时候会 rehash……"

面试官追问:"(e.hash & oldCap) == 0 这一行代码具体在判断什么?"

候选人沉默。

# 1.2 问题不在"记不住",而在"没想通"

候选人不是不看源码,而是只记结论,没画过图:

oldCap = 16 = 0b010000
newCap = 32 = 0b100000

某个 hash 值:
  ...xxxxxx Y zzzz  (Y 是第 5 位,zzzz 是低 4 位)

旧 index = hash & (16-1) = hash & 0b01111 = zzzz          (只看低 4 位)
新 index = hash & (32-1) = hash & 0b11111 = Y zzzz        (多看了第 5 位 Y)

如果 Y = 0 → 新 index = 旧 index(留在原位)
如果 Y = 1 → 新 index = 旧 index + 16(移到新位)

"hash & oldCap" 正好提取的就是这个 Y 位!
1
2
3
4
5
6
7
8
9
10
11
12
13

一旦画过这张图,这辈子都忘不了。本篇的每一道题,都给你一张"让你忘不掉"的思维图。

# 1.3 本篇的打开方式

  • 不是背答案:每题先看"这类问题考什么",再看"解题步骤",最后才看"答案要点"。
  • 不是穷举:只挑 19 篇中最具代表性、最容易暴露理解深度的题目。
  • 不是标准答案:给出思考框架和关键要点,鼓励你补充自己的理解。

# 02. 内功题时空复杂度

# 2.1 代表题:以下代码的复杂度是多少?

// 题1:ArrayList 头插 N 次
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(0, i);

// 题2:LinkedList 下标遍历
for (int i = 0; i < linkedList.size(); i++) linkedList.get(i);

// 题3:HashMap 的 containsValue
map.containsValue(target);
1
2
3
4
5
6
7
8
9

# 2.2 解题步骤

flowchart LR
    A[识别底层结构] --> B[定位单次操作成本]
    B --> C[考察循环/递归如何叠加]
    C --> D[是否有均摊?<br/>是否有 cache 局部性?]
1
2
3
4

# 2.3 答案要点

  • 题 1:ArrayList 每次 add(0, e) 要 System.arraycopy 移动全部已有元素,单次 O(N),总共 O(N²);10 万次 → 100 亿次元素搬移,实测约 1200ms。
  • 题 2:LinkedList.get(i) 是 O(N),循环 N 次 → O(N²);100 万元素会卡死在秒级以上。改为 for (T t : linkedList) → O(N)。
  • 题 3:containsValue 遍历所有桶的所有链表节点 → O(N)。对比 containsKey 是 O(1)。频繁按 value 查找应维护反向 Map。

# 2.4 延伸:均摊复杂度

ArrayList 的 add(e) 单次看可能触发扩容 → O(N);
但 N 次 add 总共只触发 log N 次扩容,均摊到每次 → O(1)。

均摊分析的关键:用"总和 / 次数"而非"最坏单次"。
1
2
3
4

# 03. 设计题扰动函数

# 3.1 代表题:为什么要 h ^ (h >>> 16)?

# 3.2 解题步骤

步骤 1:写出 HashMap 桶定位公式
  index = (n - 1) & hash

步骤 2:观察默认 n = 16 时 n-1 的二进制
  n - 1 = 0b00001111 → 只有低 4 位

步骤 3:思考"如果直接用 hashCode()"会怎样
  只有低 4 位参与,高 28 位完全浪费
  → 只要低 4 位相同的 key 就一定碰撞

步骤 4:思考"如何让高位信息也参与"
  把高 16 位异或到低 16 位
  → 一次异或,高低位都进入 index
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.3 答案要点

  • 用"一次异或"让高位信息参与低位运算,零成本大幅降低碰撞率。
  • 只做 16 位而不是每位都扰动,是性能 vs 质量的权衡:16 位足够降低碰撞,再多就是浪费 CPU。
  • JDK 8 的扰动只有一步,JDK 7 有多步(h ^= (h >>> 20) ^ (h >>> 12); h ^ (h >>> 7) ^ (h >>> 4))——简化的依据是"红黑树树化机制已经兜底"。

# 3.4 进阶:如果 n 不是 2 的幂呢?

index = hash % n    ← 取模,CPU 代价比 & 高一个数量级
       ↓
所以 HashMap 强制 n 是 2 的幂,就是为了用 & 代替 %。
扩容也只翻倍,保持 2 的幂。
1
2
3
4

# 04. 并发题原子复合

# 4.1 代表题:下面代码为什么并发不安全?

if (!map.containsKey(key)) {
    map.put(key, computeValue(key));
}
1
2
3

# 4.2 解题步骤

sequenceDiagram
    participant A as 线程 A
    participant M as ConcurrentHashMap
    participant B as 线程 B
    A->>M: containsKey(key) → false
    B->>M: containsKey(key) → false
    A->>M: put(key, valueA)
    B->>M: put(key, valueB)   
    Note right of B: valueA 被 valueB 覆盖!<br/>且 computeValue 被执行了两次
1
2
3
4
5
6
7
8
9

# 4.3 答案要点

  • ConcurrentHashMap 的每个方法都是原子的,但复合调用不是原子的——containsKey + put 两步之间,其他线程可以插入。

  • 正确写法:

    map.computeIfAbsent(key, k -> computeValue(k));
    
    1

    同一 key 的桶级 synchronized 保证 lambda 只执行一次,解决缓存击穿。

  • 更深层:computeIfAbsent 内部 synchronized (f) 锁住桶头节点,其他 key 的并发写不受影响。

# 4.4 进阶:computeIfAbsent 能在 lambda 里递归调用自己吗?

  • 不能。lambda 里再调用 map.computeIfAbsent(sameKey, ...) 会自死锁或抛 IllegalStateException(JDK 14+ 增加检测)。
  • 如果需要链式依赖,先查后写,或者用 get + putIfAbsent 组合。

# 05. 内存题LinkedList代价

# 5.1 代表题:100 万个 Integer 在 ArrayList 和 LinkedList 中各占多少内存?

# 5.2 解题步骤

步骤 1:拆解单元素的组成
  ArrayList:
    element[] 槽位 (8B 引用) + Integer 对象 (16B 头 + 4B value + 4B 对齐 = 24B)
    单元素 = 8 + 24 = 32B

  LinkedList.Node:
    对象头 16B + prev 8B + next 8B + item 8B + 对齐 = 40B
    + Integer 24B
    单元素 = 40 + 24 = 64B

步骤 2:乘以规模 + 加上容器开销
  ArrayList: 100万 × 32B = 32 MB + 扩容冗余 25% ≈ 40 MB
  LinkedList: 100万 × 64B ≈ 64 MB(无扩容冗余)

步骤 3:得出结论
  LinkedList 比 ArrayList 多用 60% 内存。
  规模越大差距越绝对。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 5.3 答案要点

  • LinkedList 每节点 40B 纯开销,是 ArrayList 的 5 倍(8B/槽位)。
  • Integer 装箱是另一重开销,基本类型集合考虑 Eclipse Collections / fastutil 的 IntArrayList。
  • 小数据量 < 1000 时一切都不重要,代码可读性优先。

# 06. 设计题LRU五行实现

# 6.1 代表题:为什么 removeEldestEntry 能让 LRU 代码优雅到 5 行?

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxSize;
    public LRUCache(int maxSize) {
        super(maxSize, 0.75f, true);  // accessOrder = true
        this.maxSize = maxSize;
    }
    @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxSize;
    }
}
1
2
3
4
5
6
7
8
9
10

# 6.2 答题框架

问题抽象:
  LRU = HashMap 的 O(1) 查 + 双向链表的 O(1) 淘汰

现成材料:
  LinkedHashMap 已经 = HashMap + 双向链表
  accessOrder=true 让 get/put 自动把节点移到尾部

关键钩子:
  每次 put 后,HashMap 回调 removeEldestEntry(head)
  返回 true 就删除链表头(最久未访问)

结果:
  get → O(1)
  put → O(1)
  淘汰 → O(1)
  总代码 5 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6.3 答案要点

  • 复用 > 重新发明:与其手写双链表 + HashMap,不如继承 LinkedHashMap。
  • 模板方法模式:父类定义流程(put → 判断 → 删除),子类只实现决策点。
  • 缺陷:不是线程安全;生产环境推荐 Caffeine(W-TinyLFU,命中率更高)。

# 07. 跨语言Go与Java

# 7.1 代表题:Go 标准库没有 Set / LinkedList / TreeMap,为什么?

# 7.2 答题要点

  • 设计哲学:Go 强调"最小惊讶"和"少即是多",不在标准库堆砌。
  • 实用替代:
    • Set → map[T]struct{}(struct{} 零字节)
    • LinkedList → container/list(标准库但很少人用,实战用 slice 替代)
    • TreeMap → 无内置,用 github.com/google/btree 或排序 slice + 二分
  • 性能优先:Go 的 slice(动态数组)已经覆盖 90% 场景,多一种数据结构 = 多一种选择困难 + 多一份维护成本。
  • 对比 Java:Java 集合框架教科书级别,但也背负了 Hashtable / Vector / Stack 等历史包袱。

# 7.3 两种哲学的取舍

哲学 优势 劣势
内置丰富(Java) 开箱即用、标准化 API 历史包袱、设计失误难纠正
保持简洁(Go) 语言小、学习曲线低 社区碎片化、同一个需求多种轮子

# 08. 容量题HashMap初始化

# 8.1 代表题:要存 10000 个元素,HashMap 初始容量填多少?

# 8.2 公式推导

核心约束:size ≤ capacity × loadFactor 时不扩容
即:capacity ≥ size / loadFactor

initialCapacity = (int) (expectedSize / 0.75f) + 1
                = (int) (10000 / 0.75f) + 1
                = 13334

HashMap 会自动向上取 2 的幂 → 16384
此时 threshold = 16384 × 0.75 = 12288,足够放 10000 个元素,零扩容。
1
2
3
4
5
6
7
8
9

# 8.3 一行优雅写法

// JDK 19+
Map<String, Object> map = HashMap.newHashMap(10000);

// Guava
Map<String, Object> map = Maps.newHashMapWithExpectedSize(10000);

// 手写
Map<String, Object> map = new HashMap<>((int) (10000 / 0.75f) + 1);
1
2
3
4
5
6
7
8

# 09. 架构题百亿URL去重

# 9.1 代表题:URL 去重单机 HashSet 放不下(~480GB),怎么办?

# 9.2 三层方案

flowchart LR
    URL[URL 请求] --> B[第1级: BloomFilter]
    B -->|不存在| NEW[新URL, 直接入队]
    B -->|可能存在| P[第2级: 分片精确 Set]
    P -->|确认存在| DUP[已去重]
    P -->|不存在| SYNC[写回分片 + 布隆]
1
2
3
4
5
6

# 9.3 参数设计

层级 结构 容量 内存 假阳/假阴
1 BloomFilter(1% 误判) 100 亿 ~12 GB(单机) 可能假阳性
2 分片 HashSet(10 台) 10 亿/台 ~48 GB/台 精确
3 可选:HBase/RocksDB 全量 磁盘 精确持久化

# 9.4 答案要点

  • BloomFilter 永不漏判(false negative = 0),只会假阳性,完美适配"先判再查"场景。
  • 分片 Hash + 一致性哈希:扩容时只迁移 1/N 的数据。
  • HyperLogLog:如果只要计数(有多少不同 URL)而不是"是否存在",HLL 只要 KB 级内存。

# 10. 本篇收获案例回扣

回到开篇候选人的HashMap 扩容翻车:

问题 解题范式 本篇章节
为什么 hash & oldCap 判断新位置? 画二进制图,盯住多出来的那一位 §3.2
复合操作为什么非原子? 画 sequence diagram,看两步之间的"时间缝" §4.2
内存怎么估? 拆分对象头+字段+对齐,乘规模加冗余 §5.2
容量怎么算? 从 threshold 公式反推 §8.2

本篇核心收获:

  • 答题四板斧:识别结构 → 定位操作成本 → 累加到规模 → 给量化结论。
  • 把抽象画成图:二进制位图、sequence 时序、桶-链-树结构图——一旦画过,终生不忘。
  • 背后的设计哲学:复用优于重造(LRU 5 行)、简洁优于齐全(Go 无 Set)、零成本优于优雅(异或扰动)。
  • 工程取舍矩阵:时间 / 空间 / 并发 / 可读性 / 一致性 / 持久化,挑两三个做主战场,其他做牺牲。
  • 架构级思维:单机装不下 → 分层(布隆+精确)→ 分片(水平扩展)→ 降级(HLL 估算)。

# 11. 思考题给你自己命题

真正学通的标志不是"我会答别人的题",而是"我能出好题"。

1. 出一道你认为能筛掉 90% 候选人的 HashMap 题:题目要让"只背 API 的人"必死,让"画过图的人"秒杀。附上你的解题思路和评分标准。

2. 写一篇"我犯过的最蠢的集合选型错误":不要只写错误本身,要分析"当初为什么觉得对"、"怎么被发现"、"修复后指标变化"。

3. 挑选 19 篇中的任意一篇,给它出 3 道由浅入深的思考题,覆盖"记忆 / 理解 / 应用 / 分析 / 评价 / 创造"这 6 个认知层级中的至少 4 个。

4. 挑一个你熟悉的业务系统,画出它用到的所有集合,标注每个集合的选型理由,指出至少一个"现在看不太合理"的选型,提出优化方案。

5. 面试官视角:假设你要面试一个 5 年经验的候选人,只能问 3 道数据结构问题,你会问哪 3 道?为什么?用"考察点-区分度-参考答案"三元组写出。

# 12. 课后作业实战

# 作业 1:建立你的"错题本"

把本专栏 19 篇每篇的思考题抽出来,做成一张 Notion / 飞书文档,包含:

  • 题干 + 所属章节 + 核心考点
  • 你第一次的答题(不要看答案)
  • 参考答案要点
  • 你的反思与补充

坚持做完所有 100+ 道题,你的数据结构功底会直接上一个台阶。

# 作业 2:画 20 张"让你忘不掉"的图

选你最容易忘的 20 个知识点,每个画一张图:

  • HashMap 扰动函数的位运算图
  • ConcurrentHashMap JDK8 put 时序图
  • LinkedHashMap LRU 双链表状态变化图
  • 红黑树 5 规则示意图
  • ArrayList 扩容前后数组布局图
  • ……

画完后贴在工位或 wiki,自测"不看图能说清原理吗"。

# 作业 3:面试 / 被面试

找一位同事或朋友,互相用本专栏的题库做白板面试。你出题、他答题,反之亦然。重点关注:

  • 他是"背答案"还是"现场推导"?
  • 你能否仅凭"他的解题步骤"判断理解深度?
  • 如何用一个追问暴露"只记结论没想通"?

三轮模拟之后,你会对"如何考察数据结构功底"有全新的体会。

# 作业 4:LeetCode 终极刷题清单(Top 50)

覆盖本专栏所有核心结构,按由易到难排序。要求每题都能"口述复杂度 + 画出数据流"。

数组 & 字符串:1, 15, 11, 42, 238, 560, 76, 76, 4, 239 链表:206, 21, 141, 142, 25, 92, 148, 160 栈 & 队列:20, 155, 232, 739, 84, 85, 496, 503 Hash:1, 49, 128, 138, 146, 460, 380, 3 树 & 堆:94, 104, 102, 236, 297, 215, 347, 295 递归 & 回溯:46, 78, 39, 51, 22, 79, 140

要求:每刷完 10 题,写一篇"我踩过的坑"短记,半年后回看你会感谢自己。


至此,数据结构算法专栏全部 20 篇落幕。*真正的成长不在读完,而在接下来你写的每一行代码里。

# 13. 进阶专题与延伸

作为整个系列的压轴,本节把视野拉开:面试之外,数据结构还有哪些"新前沿"?从 CS 课程的经典读物、到分布式/操作系统里的数据结构、到正在工业落地的研究性结构,我们梳理一份"持续精进"的地图。

# 13.1 一张图看懂整个系列

                   【数据结构算法 20 篇地图】
                              │
       ┌──────────┬──────────┼──────────┬──────────┐
       │          │          │          │          │
    线性结构   树形结构   哈希结构   递归思想    集合应用
   (03-07)   (08-09)   (11-12)    (10)      (13-19)
       │          │          │          │          │
    数组·链   二叉树·    Hash·散    分治·回    List·Map·
    表·栈·    红黑树     列        溯         Set + 选型
    队列                                       
       │                                       │
       └───────┬───────────────────────┬───────┘
               │                       │
           底层原理                 工业实现
        (算法书角度)           (源码+性能调优)
               │                       │
               └───────────┬───────────┘
                         思考题 (20)
                   串起「知识点」→「工程直觉」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

三条主线:

  1. 从数据到结构:数组 → 链表 → 栈/队列 → 树 → 图
  2. 从结构到算法:递归 → 哈希 → 排序 → 搜索
  3. 从算法到工程:JDK 集合源码 → 选型与调优 → 分布式扩展

# 13.2 数据结构的"前沿"——读完系列后还能学什么

1. 概率数据结构(Probabilistic Data Structures)

  • Count-Min Sketch:近似频次统计,空间亚线性(Twitter Heron、Google AdSense 用)
  • t-digest:流式数据求分位数(P50/P99),Elasticsearch、Spark 底层
  • Skip Graph / Skip List+:分布式哈希环的变种
  • 书:Probabilistic Data Structures and Algorithms for Big Data Applications(Andrii Gakhov)

2. 持久化数据结构(Persistent Data Structures)

  • Finger Tree:支持 O(log n) 的任意位置插入
  • RRB-Tree:Scala/Clojure 的 Vector 演进版,支持 O(log n) 拼接
  • Persistent Red-Black Tree:OCaml 标准库的 Map 实现
  • 论文:Okasaki Purely Functional Data Structures(MIT Press 1998,神作)

3. 并发无锁数据结构(Lock-Free / Wait-Free)

  • Michael-Scott Queue(MS 队列):经典无锁 FIFO
  • Harris Linked List:无锁有序链表(第 4 篇已述)
  • Hazard Pointer:无锁内存回收方案
  • RCU(Read-Copy-Update):Linux 内核核心机制
  • 书:Herlihy & Shavit The Art of Multiprocessor Programming

4. 外存 / 分布式数据结构

  • B+ Tree:所有关系型数据库索引(MySQL InnoDB / PostgreSQL)
  • LSM-Tree:RocksDB / LevelDB / Cassandra / HBase(第 8 篇已述)
  • Merkle DAG:Git / IPFS / 区块链
  • CRDT(Conflict-free Replicated Data Type):协作编辑(Figma / Yjs)
  • 书:Kleppmann Designing Data-Intensive Applications("DDIA",人手一本)

5. 空间数据结构

  • KD-Tree / R-Tree:地理位置索引(PostGIS / MongoDB)
  • Quadtree / Octree:游戏碰撞检测、地图分级
  • Geohash:滴滴/美团的附近司机查找
  • HNSW:向量数据库的近似最近邻(Milvus / Pinecone / LLM RAG)

# 13.3 操作系统里的数据结构——Linux 内核的"私藏"

场景 数据结构 文件
进程调度 红黑树 kernel/sched/fair.c
侵入式链表 list_head include/linux/list.h
定时器 哈希时间轮 kernel/time/timer.c
伙伴系统 Buddy 分配器 mm/page_alloc.c
SLUB 分配器 Per-CPU 缓存 + 链表 mm/slub.c
VFS 红黑树 + radix tree fs/dcache.c
epoll 红黑树 + 就绪双链 fs/eventpoll.c
RCU Read-Copy-Update kernel/rcu/

学习路径建议:

  1. 先读 include/linux/list.h(200 行,双向循环侵入式链表+宏魔法)
  2. 再读 lib/rbtree.c(700 行,Linux 红黑树,比 Java TreeMap 干净)
  3. 最后看 kernel/sched/fair.c(CFS 调度器,红黑树 key 是 vruntime)

# 13.4 CS 经典教材推荐(按难度递增)

级别 书名 作者 特点
入门 算法(第 4 版) Sedgewick Java 版,图多
经典 算法导论(CLRS) Cormen 等 圣经,1300 页
进阶 Purely Functional Data Structures Okasaki 函数式视角
进阶 The Art of Multiprocessor Programming Herlihy 并发算法
实战 Designing Data-Intensive Applications Kleppmann 分布式
速查 Hacker's Delight Warren 位运算
算法 Competitive Programmer's Handbook Laaksonen 免费 PDF

# 13.5 online judge 平台推荐与刷题策略

平台 特点 适用阶段
LeetCode 题库全、社区大、中英文 找工作必备
牛客网 中文面经、模拟面试 国内应届
Codeforces 竞赛风、难度梯度 提升算法
HackerRank 企业认证、域分明 系统化训练
LintCode 覆盖 LeetCode + 独家题 进阶补漏

策略建议:

  • 三刷法:第 1 刷按类别(数组→链表→树),第 2 刷按难度(简单→中等→困难),第 3 刷按频度(高频 100/200/300)
  • 错题本优先:比"刷新题"重要 3 倍
  • 一题多解:至少想 2 种解法再看题解
  • 白板模拟:每周 1 次对着笔记本摄像头口述解题

# 13.6 企业工程实践的"数据结构选型"方法论

一个经验丰富的工程师面对"该选什么数据结构"时的思考链:

Step 1: 业务需求
  ├─ 操作频率分布(读/写/遍历/排序各多少 %)
  ├─ 数据规模(百/千/万/百万/亿)
  ├─ 数据类型(原生 int?对象?string?)
  ├─ 时延要求(P50/P99/P999)
  └─ 一致性要求(强/最终/弱)

Step 2: 候选结构初筛
  ├─ "必备功能"过滤:支持顺序?支持并发?
  └─ 保留 2-3 个候选

Step 3: 量化对比
  ├─ Big-O(理论上限)
  ├─ 常数因子(cache 友好度)
  ├─ 内存占用(header + padding + reference)
  └─ GC 影响

Step 4: 原型 + 压测
  ├─ JMH 基准(微基准)
  ├─ 业务级压测(宏基准)
  └─ 生产灰度(10% 流量)

Step 5: 长期监控
  ├─ GC 日志
  ├─ 堆快照
  └─ 火焰图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 13.7 如何在面试中"展示数据结构功底"

不是"背答案",是"展示思维过程":

  1. 复述需求:先把面试官的问题复述一遍,确认理解
  2. 列候选方案:至少说 2 种,说清每种的时空复杂度
  3. 画图讲解:白板画数据流转,比纯口述强 10 倍
  4. 讨论权衡:主动说"我选 A 是因为……但如果场景变成 B,我会改选……"
  5. Coding 前构思:边写边说每一行目的
  6. 测试边界:写完主动说"空输入/超大输入/异常输入"
  7. 复杂度总结:最后一句主动总结时空复杂度

反例(容易被刷):

  • 立刻动手写代码,不讨论思路
  • 只说"这题应该用 HashMap",不说为什么
  • 写出来后不能说清复杂度
  • 被追问"为什么这样设计"时卡壳

# 13.8 将这套知识用到业务上:四个落地方向

  1. 性能瓶颈定位:火焰图看到 LinkedList.get 就知道选错
  2. 内存优化:把 HashMap<Long, Long> 换 Long2LongOpenHashMap 可能省 80%
  3. 系统设计:限流器用什么?Deque<Timestamp> 滑动窗口 vs 令牌桶 AtomicLong
  4. 代码评审:看到 new HashMap<>() 问"容量有预估吗",看到 synchronized 问"能否 CAS"

# 13.9 结语:数据结构是工程师的"元技能"

我们讲了 20 篇,把主流的线性、树形、哈希、集合结构都过了一遍,从数组、链表、栈、队列到红黑树、哈希表、散列表、LRU。从底层原理到 JDK 源码到选型决策到性能调优,知识密度确实大。

但要记住三句话:

"结构比算法更重要"——好的数据结构能让算法自然而然;差的结构让天才算法也吃力。

"选型比写代码更重要"——新手关心"能不能跑";老手关心"该用哪个";架构师关心"长期会怎样"。

"源码比教材更重要"——JDK 集合源码是亲身上线的工业级代码,任何教材都无法替代。

至此,本专栏真正收尾。数据结构不是一本能读完的书,它是你未来每一行代码里的内功。下次评审一段代码、设计一个系统、排查一个线上事故时——希望你能想起这 20 篇中的某一段、某一张图、某一次"原来如此"的顿悟。

— 共勉。

上次更新: 2026/06/17, 12:46:05
算法工程化案例
README

← 算法工程化案例 README→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式