如果你刷过一段时间算法题,大概率遇到过这些争论:有人说 KMP 本质是动态规划,有人反驳说它是自动机;有人说背熟堆排序模板就能应付面试,有人写快排却连边界条件都要现场推半小时。算法板子这四个字,在这些讨论里要么被神化,要么被当成死记硬背的反面教材。作为一名靠算法竞赛入行、又做了多年面试官的人,我的态度很朴素:板子确实要背,但更要搞清楚该背哪些、背到什么程度、以及用什么方式去背才不会变成“搬砖式默写”。
这篇内容就是围绕“必背算法板子”来写的。我会结合近期的算法热词里大家最关心的排序、二分、KMP、Dijkstra、并查集、DP、贪心等方向,把真正值得背的模板梳理出来,同时把哪些只需要理解流程、哪些根本不该硬背也一并讲清楚。文章不会照着教科书念定义,更多是给你一份可以直接落地的学习清单和避坑指南。
1. 算法板子的边界:不是所有算法都值得“背”
很多人一开始就把方向搞反了。他们把所有算法统统当成板子去背,结果越背越乱,最后连“排序算法为什么这样写”都答不上来。实际上,算法知识至少可以分成三层,每层的投入策略完全不同。
1.1 第一层:模板化算法,值得反复默写
这一层的典型特征是:核心流程非常固定,输入输出格式比较统一,且边界条件容易出坑。比如归并排序、二分查找、快速幂、KMP、并查集、Dijkstra、二叉树遍历、拓扑排序等。这些算法在笔试、面试和竞赛题里都会被高频复用,几乎不存在“换一个业务场景就完全不用”的情况。
对这些算法,我会要求自己做到闭着眼睛能写出完整代码,并且能解释每一行为什么这么写。因为你一旦在考场上现推边界条件,时间根本不够用;更重要的是,这些模板是所有复杂问题的底座,底座不牢,后面做图论、树形 DP、字符串处理都会到处漏风。
1.2 第二层:算法思想,要能讲原理和流程
贪心、动态规划、回溯、分治这类东西,往往没有唯一的“标准模板”,因为同一思想在不同题目里长着完全不同的面貌。你背一个“区间调度贪心的代码”,换一道“求最多不相交区间”的变体题,代码几乎无法复用;你能复用的是“按结束时间排序”的思维方式。
所以对这种类型,我会把重点放在证明和方法上,而不是背代码。比如贪心算法,最核心的是证明贪心选择安全性:当前这一步取最优解,为什么不会影响后续步骤?很多人说贪心靠猜,其实猜完必须用归纳法或者反证法证明,证明能过,代码几乎都是顺理成章的。
1.3 第三层:工程化算法,要背的是设计原则而不是固定代码
热词里出现了 PID、模拟退火、粒子群、ISP Bayer2RGB、AES、国密 SM 系列等,这些在日常工作中经常被提到,但它们绝大多数并不适合当成“板子”来死记。PID 是控制框架,参数 Kp、Ki、Kd 都需要根据被控对象在线整定;模拟退火和粒子群是元启发式算法,写成代码时和具体问题耦合极深,没有统一范式;AES、SM4 这类密码算法更是有标准库和官方文档,生产环境里自己手写反而容易引入安全漏洞。
对于这层内容,正确的学习方式是理解输入输出、弄清适用条件和适用边界、掌握调用方式或调优思路。
| 算法示例 | 正确姿态 | 原因 |
|---|---|---|
| 归并排序、二分查找 | 背到肌肉记忆 | 流程固定,边界容易踩坑 |
| 贪心、动态规划 | 背套路,练推导 | 核心在模型识别和状态设计 |
| PID、模拟退火、深度学习 | 背原理和适用条件 | 高度依赖场景,没有通用代码 |
| AES、SM4 | 直接用成熟库 | 自己实现容易出错且安全风险高 |
1.4 只会默写,不理解原理,才是背板子的最大风险
我面试时遇到过一个候选人,快排代码写得非常流利,但问他“快排最坏情况下复杂度是多少、什么时候发生”时,他愣了一下,然后说没考虑过。这就是把板子背成了“死模板”。
为了不陷入这种状态,每一个需要背的模板我都会强迫自己回答三个问题:它解决什么问题?边界条件在哪里?为什么复杂度是这个量级?这三个问题的答案我用注释写在模板代码顶部,背板子的同时也就把原理捋了一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一梯队模板:排序、二分与快速幂的代码骨架
如果说算法板子有一座金字塔,那最底层一定是这几个:排序、二分、快速幂。它们不仅在竞赛里出现频率极高,也是面试手写代码最常抽查的基础能力。下面逐个拆解我最常用的骨架和易错点。
2.1 归并排序:背的是分治框架,顺带解决逆序对
归并排序的代码模板比较长,但结构非常固定,核心就三步:拆半、递归、合并。我用 C++ 写一个最常用的版本,函数里附带统计逆序对数的功能,这是竞赛题里特别常见的用法。
cpp复制void mergeSort(vector<int>& a, int l, int r, long long& inv) {
if (l >= r) return;
int mid = ((l + r) >> 1);
mergeSort(a, l, mid, inv);
mergeSort(a, mid + 1, r, inv);
vector<int> tmp(r - l + 1);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
tmp[k++] = a[j++];
inv += mid - i + 1; // 左半段剩余的都比 a[j] 大
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (int t = 0; t < k; ++t) a[l + t] = tmp[t];
}
这里有个细节值得多说一句:统计逆序对时必须在右半段元素准备放入临时数组的那一刻,把 mid - i + 1 累加进答案。原因在于左半段和右半段此时都已经有序,如果左半段当前的 a[i] 大于右半段的 a[j],那么左半段从 i 到 mid 的每一个数都大于这个 a[j],这就是逆序对的数量。
逆序对计数的返回类型建议直接写成 long long。数据量到十万级别时,逆序对数量可能轻松突破 int 上限,我用 int 被坑过不止一次。
2.2 快排:边界处理比算法思想更容易翻车
快速排序的思想几乎所有人都能说清,但能把边界收敛写成零 bug 的人并不多。我比较推荐一种“双指针挖坑”写
