刷了 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 = 解题能力

核心认知

算法的价值不在于你能不能手写红黑树,而在于你能不能在遇到问题时,想到用什么数据结构、什么算法来解决。

下一篇,我们从最基础的开始:排序与二分查找——它们比你想象的更有用。


下一篇:排序与二分:被低估的基础功

本系列:

  1. 算法是业务武器(本篇)
  2. 排序与二分:被低估的基础功
  3. 哈希表:缓存设计的基石
  4. 布隆过滤器:用 1% 误判换 90% 内存
  5. 一致性哈希:分布式系统的路由表
  6. 堆与优先队列:调度器的核心
  7. 限流算法:保护系统的三道防线
  8. 跳表:Redis 排行榜的秘密
  9. 树结构:层级数据的优雅解法
  10. 字符串匹配:敏感词过滤的正确姿势
  11. 实战选型:没有银弹,只有场景