刷了 500 道 LeetCode,进了公司发现一道都用不上?不是算法没用,是你学的姿势不对。算法不是面试的敲门砖,而是解决业务问题的武器。
一个真实的故事
2019 年,某电商平台大促期间,缓存集群扩容后出现大面积缓存穿透。
现象:
- 扩容前:4 台缓存服务器,命中率 95%
- 扩容后:8 台缓存服务器,命中率 40%
结果:
- 数据库直接被打爆
- 大促第一个小时,损失数千万
技术负责人盯着监控,冷汗直流。
问题出在哪?路由算法。
原来的路由:hash(key) % 4
扩容后的路由:hash(key) % 8
key = "user:12345"
扩容前:hash % 4 = 1 → 服务器 1
扩容后:hash % 8 = 5 → 服务器 5
同一个 key,被路由到不同的服务器
服务器 5 没有这个 key 的缓存 → 穿透到数据库
几乎所有的 key 都重新分布了,缓存全部失效。
解决方案:一致性哈希。这不是什么高深的算法,任何一本数据结构教材都有。但它能让扩容时平均只有约 1/(N+1) 的数据需要迁移(N+1 是扩容后的总节点数),而不是像取模哈希那样几乎全部失效。
这位技术负责人后来说:“算法不是不重要,是我们学的时候没学到点子上。”
算法在业务中的三种存在形式
形式一:直接使用
有些算法你每天都在用,只是没意识到。
// 你以为你在用 HashMap
let cache: HashMap<String, User> = HashMap::new();
// 实际上你在用:
// - 哈希函数(把 key 映射到桶)
// - 开放寻址或链表(处理冲突)
// - 动态扩容(rehash)
// 你以为你在用 BTreeMap
let sorted_users: BTreeMap<i64, User> = BTreeMap::new();
// 实际上你在用:
// - B 树(自平衡多路搜索树)
// - O(log n) 的查找、插入、删除
// - 有序遍历
这些算法已经被封装好了,你不需要自己实现。但你需要知道它们的特性,才能选对数据结构。
形式二:组合应用
有些场景需要你把基础算法组合起来。
场景:实现一个 LRU 缓存
需要的操作:
- get(key):O(1) 获取
- put(key, value):O(1) 插入
- 淘汰最久未使用的:O(1) 删除
单一数据结构做不到,需要组合:
- HashMap:O(1) 查找
- 双向链表:O(1) 调整顺序、删除
HashMap + 双向链表 = LRU Cache
use std::collections::HashMap;
struct LRUCache<K, V> {
map: HashMap<K, *mut Node<K, V>>,
head: *mut Node<K, V>, // 最近使用
tail: *mut Node<K, V>, // 最久未使用
capacity: usize,
}
这种组合不是凭空想出来的,而是分析需求后,用合适的数据结构满足复杂度要求。
形式三:场景适配
有些算法需要根据业务场景调整。
场景:敏感词过滤
朴素方案:
for word in sensitive_words:
if word in content:
replace(word, "***")
10000 个敏感词 × 10000 字文章 = 1 亿次比较
优化方案:AC 自动机
- 把所有敏感词构建成一个状态机
- 一次扫描文章,O(n) 完成所有匹配
- 10000 个敏感词也是 O(n)
AC 自动机不是什么新发明,但把它用在敏感词过滤上,需要你理解它的适用场景。
为什么 LeetCode 和工作脱节?
| LeetCode | 工作 |
|---|---|
| 给你一个数组… | 给你一个分布式系统… |
| 求最优解 | 求可接受的解 |
| 时间复杂度是唯一指标 | 可维护性、可读性同样重要 |
| 输入规模固定 | 输入规模可能从 100 涨到 1 亿 |
| 单机内存 | 数据可能分布在 100 台机器上 |
LeetCode 训练的是算法思维,不是工程能力。
工程中的算法选型,要考虑:
1. 数据规模
- 100 条数据:O(n²) 也无所谓
- 1 亿条数据:O(n log n) 和 O(n) 差约 27 倍(log₂(10⁸) ≈ 26.5)
2. 访问模式
- 读多写少:可以牺牲写性能换读性能
- 写多读少:反过来
3. 一致性要求
- 金融场景:宁可慢,不能错
- 推荐场景:快比准重要
4. 运维成本
- 自己实现红黑树 vs 用 BTreeMap
- 99% 的情况选后者
这个系列要讲什么
我们不讲 LeetCode,讲业务场景中的算法应用。
每篇文章的结构:
1. 业务场景
- 你会遇到什么问题
- 朴素解法为什么不行
2. 算法原理
- 核心思想(用类比解释)
- 关键操作的复杂度
3. 代码实现
- Rust 代码(关注可读性)
- 关键步骤注释
4. 工程考量
- 什么时候用
- 什么时候不用
- 有什么坑
文章列表
| 篇目 | 主题 | 业务场景 |
|---|---|---|
| 二 | 排序与二分 | 版本回滚、数据对账 |
| 三 | 哈希表 | 缓存设计、幂等控制 |
| 四 | 布隆过滤器 | 缓存穿透、黑名单 |
| 五 | 一致性哈希 | 分布式路由、数据分片 |
| 六 | 堆与优先队列 | 延迟任务、Top-K |
| 七 | 限流算法 | 令牌桶、滑动窗口 |
| 八 | 跳表 | Redis 排行榜 |
| 九 | 树结构 | 层级数据存储 |
| 十 | 字符串匹配 | 敏感词过滤 |
| 十一 | 实战选型 | 综合案例 |
学习建议
1. 先理解问题,再学算法
错误的学习路径:
学红黑树 → 发现很难 → 放弃 → 觉得算法没用
正确的学习路径:
遇到有序 Map 性能问题 → 了解 B 树/红黑树 → 理解为什么需要自平衡 → 选择合适的实现
带着问题学,才能学进去。
2. 关注复杂度,但不要过度优化
场景:排序 100 个元素
冒泡排序:O(n²) = 10000 次比较
快速排序:O(n log n) ≈ 700 次比较
差 14 倍,但都是微秒级
花 2 小时优化,省 0.01 毫秒,不值得
过早优化是万恶之源。 先正确,再快速。
3. 多看开源项目
想学 LRU?看 Rust 的 lru crate
想学跳表?看 Redis 的 zset 实现
想学限流?看 Sentinel 的滑动窗口
比任何教程都实用
生产级代码会教你很多教科书不讲的东西:边界处理、错误恢复、性能权衡。
总结
算法不是屠龙之术,而是日常武器。
| 误区 | 真相 |
|---|---|
| 算法只在面试有用 | 算法决定系统的天花板 |
| 算法越复杂越好 | 简单算法解决 90% 的问题 |
| 要自己实现算法 | 99% 的情况用标准库 |
| LeetCode = 算法能力 | LeetCode = 解题能力 |
核心认知:
算法的价值不在于你能不能手写红黑树,而在于你能不能在遇到问题时,想到用什么数据结构、什么算法来解决。
下一篇,我们从最基础的开始:排序与二分查找——它们比你想象的更有用。
下一篇:排序与二分:被低估的基础功
本系列: