前言
算法这门学问,在很多计算机系的同学看来十分的高深。因为由老教授讲出来的知识总是由数学形式展开并做推导,在我看来这无疑是在加大算法的学习门槛。
但其实,对于工程人才的我们来说,算法真没有那么阳春白雪。它更像是我们在复杂工程实践中的总结与提炼,培养计算机思维与程序设计模式的一种抽象化的提炼。
高中时打 NOIP,大学时打 ACM,所用的战术更像是在混沌中不断训练,对于能 AC 的题只能说一句,唯手熟尔。我们的竞赛更像是传统医学的思路,完全是靠灵感来解题,知其然不知其所以然。
而到了现在,很庆幸我们生在一个幸运的时代,算法的知识与思想已经深入到工程项目的方方面面。我们不再是为了纯粹的竞赛而学习算法,而是真的可以通过精深算法来拓展我们的解决思路,优化我们的代码性能,在项目中体现自己的价值。
这也是为什么近年来一些国际大厂,例如谷歌、微软,他们在面试一位候选人时更倾向去面试算法能力而非项目经历,因为成功的业务很多都可能跟时运有关,而算法能力才能体现自己的思维能力。
现代工程开发,对于前端工程师的要求早已不只是会还原样式,调试 API 的鄙视链底层的页面仔,而是一个真正需要探索性能优化,支撑起巨型项目架构的重要一环。因此逻辑的复杂度以及可钻研的深度自然不可同日而语。因此我仍认为,所有的软件开发人员都必须对算法知识有较为深刻的理解。
而作为在职的打工人来说,我们很难花费大量时间也没有必要去阅读大部头书籍来深入算法原理。我们的目标只是理解算法与数据结构的设计思路,帮助我们在需要时运用在实际的工程项目中来优化表现。相信我,在工程项目中,想到可以优化的点要比如何去优化重要得多,也难得多。
因此我决定写一个专栏,帮助更多对此感兴趣的工程人才拓展算法思维,提升发现问题的能力。
再强调一遍,本专栏旨在提炼算法的思想,拓展技术视野,提升自己的项目质量,而非囿于某一道具体的 LeetCode 题目是否能做得出来为我的写作目标。不要从页面仔转变为做题家,要担得起工程师的称谓。
学习路线图
本专栏的章节按知识依赖关系排列,分为五个阶段。每个阶段的知识都建立在前一阶段之上:
阶段一:基础 阶段二:数组技巧 阶段三:搜索与穷举
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. 递归 │ │ 3. 排序 │ │ 8. 回溯 │
│ ↓ │ │ 4. 双指针 │ │ ↓ │
│ 2. 二叉树 │────────────→│ 5. 二分查找 │ │ 9. DFS │
│ (树上DFS/BFS)│ │ 6. 滑动窗口 │ │ ↓ │
└─────────────┘ │ 7. 前缀和 │ │ 10. BFS │
│ └──────────────┘ │ ↓ │
│ │ 11. 图论 │
└────────────────────────────────────────────────→└──────────────┘
│
阶段五:进阶 阶段四:动态规划
┌──────────────┐ ┌──────────────┐
│ 15. 单调栈 │←────────────│ 12. DP 基础 │
└──────────────┘ │ 13. DP 背包 │
│ 14. DP 子序列 │
└──────────────┘阶段一(递归 → 二叉树)是一切的根基。递归是算法世界的"原子操作",而二叉树是递归思维最直观的载体——理解了二叉树上的 DFS(前中后序)和 BFS(层序),后面所有的搜索、回溯、动态规划都能找到影子。
阶段二(排序 → 双指针 → 二分 → 滑动窗口 → 前缀和)是一组相对独立的数组处理技巧。它们不强依赖阶段一,但排序和双指针是后续许多算法的前置工具。
阶段三(回溯 → DFS → BFS → 图论)是搜索与穷举的系统化学习。回溯在隐式决策树上穷举方案,DFS/BFS 将搜索扩展到网格和图,图论则在此基础上处理环检测、拓扑排序等结构性问题。
阶段四(贪心 + 动态规划三章)是最需要刻意练习的部分。贪心是一种"每步只取局部最优"的策略,能用贪心的题不需要 DP;而 DP 的本质是"带记忆的穷举",如果先学好了递归和回溯(暴力穷举),DP 就是在此基础上加缓存和优化。贪心放在 DP 之前,正好帮你建立"先试贪心,不行再 DP"的判断直觉。
阶段五(单调栈等)是独立的高级技巧,可以在前四个阶段完成后按需学习。
为什么这样分组?
这个分组背后有一条清晰的逻辑链:所有算法要么在做搜索,要么在做优化,而优化的前提是理解搜索。
我们可以把所有算法按"搜索空间"和"搜索方式"两个维度来分类:
| 搜索空间 | 暴力搜索 | 智能搜索/优化 |
|---|---|---|
| 线性结构(数组/链表) | 遍历、枚举 | 双指针、二分查找、滑动窗口、前缀和 |
| 树形结构(二叉树/多叉树) | 前中后序 DFS、BFS | 剪枝、BST 有序性利用 |
| 隐式决策树(排列/组合/切割) | 回溯(穷举所有方案) | 剪枝(提前排除不合法分支) |
| 网格/图 | DFS(连通分量)、BFS(最短路径) | Dijkstra(有权最短路)、拓扑排序 |
| 状态空间(子问题重叠) | 递归穷举 | 动态规划(记忆化/状态转移) |
从这张表可以看出:
- 阶段一教你认识搜索空间中最基本的两种——线性递归和树形递归。
- 阶段二教你在线性结构上用各种技巧缩小搜索范围(双指针收缩、二分砍半、窗口滑动、前缀和预处理)。
- 阶段三教你在树形/图形结构上搜索——先是隐式决策树(回溯),再是显式网格(DFS/BFS),最后是一般图(图论)。搜索空间从简单到复杂,逐步递进。
- 阶段四则是搜索的终极优化——当暴力搜索中存在大量重叠子问题时,用 DP 缓存结果,把指数级复杂度压缩到多项式级。
换句话说:先学会暴力穷举(阶段一、三),再学会聪明地减少计算量(阶段二、四)。这就是本专栏的学习主线。
拿到一道题,该用什么算法?
刷题中最难的一步往往不是写代码,而是识别题型——看到题目后知道该用哪种算法。下面这棵决策树覆盖了本专栏所有主要算法,帮助你在拿到一道新题时快速定位方向。
拿到一道算法题
│
├─ 问的是"所有方案 / 所有组合 / 所有排列 / 所有路径"?
│ └─ 穷举类问题
│ ├─ 在隐式决策树上选择?(子集/排列/组合/切割)
│ │ └─ 回溯 ......................................... → 回溯算法
│ ├─ 在网格上搜索连通区域?(岛屿/Flood Fill)
│ │ └─ DFS .......................................... → 深度优先搜索
│ └─ 在图上搜索路径?(DAG 所有路径)
│ └─ 图 DFS + 回溯 ................................ → 图论算法
│
├─ 问的是"最短 / 最少 / 最近"?
│ ├─ 无权图/网格的最短路径?
│ │ └─ BFS ............................................. → 广度优先搜索
│ ├─ 有权图的最短路径?
│ │ └─ Dijkstra ........................................ → 图论算法
│ └─ 最优值可由子问题推导?(最少硬币/最短编辑距离)
│ └─ 动态规划 ........................................ → 动态规划三章
│
├─ 问的是"最大 / 最多 / 最优"且每步只需局部最优?
│ ├─ 能证明局部最优 → 全局最优?
│ │ └─ 贪心 ............................................ → 贪心算法
│ └─ 不确定?先试贪心,反例则转 DP
│ └─ 动态规划 ........................................ → 动态规划三章
│
├─ 问的是"是否存在 / 能否达成"?
│ ├─ 有"选或不选"的结构?(背包/分割等)
│ │ └─ 动态规划 ........................................ → 动态规划(背包)
│ ├─ 图中是否有环 / 能否二分?
│ │ └─ 图 DFS .......................................... → 图论算法
│ └─ 搜索状态空间?(转盘锁/单词接龙)
│ └─ BFS ............................................. → 广度优先搜索
│
├─ 数据已排序,或可以通过排序简化?
│ ├─ 查找目标值 / 查找边界 / 查找满足条件的最值?
│ │ └─ 二分查找 ........................................ → 二分搜索专题
│ ├─ 两数之和 / 有序数组去重 / 链表操作?
│ │ └─ 双指针 .......................................... → 双指针问题
│ └─ 需要完整排序?
│ └─ 排序算法 ........................................ → 排序算法
│
├─ 涉及连续子数组 / 子串?
│ ├─ 求满足条件的最长/最短连续子串?
│ │ └─ 滑动窗口 ........................................ → 滑动窗口算法
│ └─ 求区间和 / 区间统计?
│ └─ 前缀和 .......................................... → 前缀和算法
│
├─ 涉及"下一个更大/更小元素"?
│ └─ 单调栈 ............................................. → 单调栈算法
│
├─ 涉及二叉树 / BST?
│ └─ 二叉树遍历 ......................................... → 二叉树遍历算法
│
└─ 涉及依赖排序 / 任务调度?
└─ 拓扑排序 ........................................... → 图论算法几条跨算法的核心思想
以上决策树看似分支繁多,但背后贯穿着几条反复出现的底层思想。理解它们,比记住每一个具体算法更重要:
1. 递归是一切搜索类算法的基石。 回溯、DFS、BFS(可用递归实现)、分治、树遍历、甚至动态规划的暴力版本——都是递归。区别仅在于递归的方向(自顶向下 vs 自底向上)和是否缓存(记忆化 vs 不缓存)。当你不知道怎么下手时,先写出暴力递归解,再考虑优化。
2. "穷举 + 剪枝"与"穷举 + 缓存"是两条主线。 回溯 = 穷举所有方案 + 用条件剪枝掉不合法的分支。动态规划 = 穷举所有子问题 + 用缓存避免重复计算。两者的出发点都是穷举,优化手段不同。如果一道题求"所有方案",通常是回溯;如果求"最优值"且子问题有重叠,通常是 DP。
3. "缩小搜索空间"是优化的统一视角。 二分查找每一步砍掉一半的搜索空间;双指针从两端向中间收缩搜索范围;滑动窗口在一维上维护一个动态的有效区间;回溯的剪枝砍掉不合法的决策分支;DP 的状态转移把指数级搜索空间压缩到多项式级。形式不同,思想一致。
4. BFS 和 DFS 选哪个?看你要"最短"还是"所有"。 需要最短路径/最少步数 → BFS(按层扩散,首次到达即最优)。需要穷举所有路径/方案 → DFS/回溯(深入到底再回溯)。需要搜索连通区域 → DFS 通常更简洁。这个选择规则适用于树、网格、图、抽象状态空间上的所有搜索问题。
5. 排序是降低复杂度的万能预处理。 回溯去重需要排序;双指针需要有序数组;二分查找的前提是有序;归并排序的 merge 阶段可以顺便统计逆序对。当题目感觉无从下手时,先想想"排序后会不会变简单"。