算法岗春招笔试复盘:KMP到并查集,考点拆解与避坑指南
2026/8/29 14:57:36 网站建设 项目流程

春招笔试是最讲性价比的地方:筛人快、题量大,很多同学在简历上花的心思比准备笔试多得多,结果笔试直接挂掉。去年我经历了2023年度小满春招算法岗第二批笔试,这场考试基本把算法岗笔试的经典考点都覆盖了一遍,从KMP的next数组到数据结构排序算法,再到贪心、快速幂、堆排序、并查集,最后还有机器学习方向的基础题。如果你正在准备算法岗春招或者暑期实习笔试,这篇复盘值得好好看完,我会把这场笔试背后的考点逻辑、题目拆解思路和踩过的坑一次性讲清楚。这篇内容不只适合马上要笔试的同学,也适合想系统巩固算法基础的开发者,因为很多经验和判断方法,刷题软件里学不到。

1. 小满春招算法岗第二批笔试:题型分布与备考重点

1.1 这场笔试到底考了什么

小满这批笔试是春招第二波,整体节奏比第一批明显更快。我印象里是限时180分钟,选择题和填空题占40分左右,编程题三道共60分,另外还有一道机器学习方向的开放题计入总分。这个配置非常典型,很多中大型公司算法岗笔试都爱这么干:一半基础题筛基础,一半编程题看代码能力,最后还塞一道模型题看你有没有真做过训练。

第一轮笔试看的是知识面广度,第二轮看的就是深度。今年算法岗竞争比往年更卷,笔试通过率大概只有20%不到,所以不能抱侥幸心理。我这里凭印象整理一下题型分布:选择题里字符串、排序、贪心、图论都有;编程题两道偏传统算法,一道偏工程模拟;附加题则涉及变分推断和模型优化。整体看下来,考察方向和搜索热词里高频出现的KMP算法、排序算法、贪心算法、动态规划、快速幂算法、并查集这些方向高度吻合。

建议第一次参加算法岗笔试的同学,先不要急着刷难题,把这场笔试暴露出的考点矩阵列出来,按优先级逐个击破。我后面会详细拆每类题的做题思路,但首先你得知道:算法岗笔试不是竞赛,是筛选,求稳比求快重要得多。

1.2 考点分布与复习优先级排序

我考完当天就把考过的知识点列了一张表,对照着复盘复习重点,这张表对你的备考规划应该也适用。

考点出现形式优先级
KMP算法(next数组计算)选择题、填空题
数据结构排序算法(快排、堆排、归并等)选择题、填空题
贪心算法编程题
动态规划编程题
快速幂、位运算编程题
并查集(带权)编程题
堆排序与优先队列编程题
机器学习基础(KL散度、ELBO、过拟合)附加题高(算法岗)
粒子群、卡尔曼滤波等启发式算法开放题低(视岗位方向而定)

从表格能看出来,笔试不是刷题越多越好,而是把高频考点覆盖掉,再准备一块自己投递方向相关的知识。我当时复习的重心全放在数据结构和经典算法上,机器学习只看了交叉熵和反向传播,结果附加题考了ELBO推导,直接给我整懵了。如果提前知道算法岗笔试必考这些,考前一周抽一个晚上把公式推导过一遍,至少能多拿十分。

2. KMP的next数组和排序算法:选择题里的高频考点

2.1 next数组的两种定义,笔试最容易翻车

KMP算法几乎是算法岗笔试选择题的钉子户。这次考了一道题,给了一个模式串 p="abacaba",问 next 数组是什么。我一开始看这题觉得很简单,但后来和同场考的人对了答案,发现大家的答案五花八门,问题就出在题目对 next 数组的定义上。

网上讲 KMP 的时候,next 数组有两种主流定义。第一种是前缀函数,也就是 pi[i] 表示 p[0..i] 这个子串的最长公共真前后缀长度。拿 "abacaba" 来算,结果如下:

  • p[0..0] = "a",最长公共前后缀长度为0
  • p[0..1] = "ab",前缀"a"和后缀"b"对不上,是0
  • p[0..2] = "aba",前缀"a"等于后缀"a",是1
  • p[0..3] = "abac",前缀和后缀对不上,是0
  • p[0..4] = "abaca",前缀"a"等于后缀"a",是1
  • p[0..5] = "abacab",前缀"ab"等于后缀"ab",是2
  • p[0..6] = "abacaba",前缀"aba"等于后缀"aba",是3

所以前缀函数结果是 [0, 0, 1, 0, 1, 2, 3]。

第二种是失配跳转数组,很多教材里也直接叫 next 数组,表示第 i 位失配时模式串指针应该跳回哪个位置,通常定义为 next[0] = -1,next[i] = pi[i-1]。这样算出来的结果是 [-1, 0, 0, 1, 0, 1, 2]。如果有的地方把 next[0] 初始化为0,结果又会是 [0, 0, 0, 1, 0, 1, 2]。

同样的字符串,三种答案可能都有人写,但只要题目有个括号说明"next[i]定义为……",答案就唯一了。我建议大家平时刷题时就练成习惯,先圈出定义再动手算,不要拿到题就默认自己熟悉的版本。

2.2 排序算法速查表与稳定性记忆法

排序算法这场笔试考得不算难,但考得很细,有两道选择题分别问"哪个排序在最坏情况下时间复杂度是O(n²)"和"哪些排序是稳定的"。这种题就是送分题,只是很多人记混。我用一张速查表把主流排序捋一遍:

排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n²)O(1)稳定
选择排序O(n²)O(n²)O(1)不稳定
插入排序O(n²)O(n²)O(1)稳定
希尔排序O(n^1.3)左右O(n²)O(1)不稳定
快速排序O(n log n)O(n²)O(log n)不稳定
归并排序O(n log n)O(n log n)O(n)稳定
堆排序O(n log n)O(n log n)O(1)不稳定
计数排序O(n+k)O(n+k)O(k)稳定

稳定性这块我自己总结过一个口诀:稳定排序只有"插归冒计基",也就是插入、归并、冒泡、计数、基数,其他常见排序基本都不稳定。笔试如果考选择题,直接用这个口诀能省好多时间。

另外注意快速排序虽然平均是 O(n log n),但最坏情况是 O(n²),当数组基本有序且选第一个元素作为基准时最容易触发。归并排序则稳定且最坏也是 O(n log n),代价是空间 O(n)。理解了这些区别,选择题基本不会失分。

2.3 贪心策略怎么判断“能不能贪”

贪心算法这次出现在编程题里,题型很经典,是一道区间调度相关的题。很多同学遇到贪心就发怵,主要是不知道什么时候可以用贪心,什么时候要用动态规划。

我的判断方法很简单:如果每一步做一个"局部最优"的决策,最终能得到全局最优,并且能举出反例证伪,那大概率可以贪。比如区间调度问题,按结束时间排序就是典型的局部最优:结束越早,剩余可用时间越多,能安排的区间数就越多。如果你按开始时间排序,或者按区间长度排序,很快就能举出反例,所以那种排法不对。

笔试时间紧张,不太可能严格证明贪心正确性。我的建议是先用小规模数据在脑子里模拟一遍,确认没有明显反例,就直接写代码。如果心里没底,就用暴力搜索跑几个随机小数据对比验证,这也算是笔试里的一个实用技巧。贪心题写起来通常很快,但前提是排序的依据想清楚,这是整个题的核心。

3. 笔试编程题拆解:快速幂、并查集、DP和堆排序的实战思路

3.1 快速幂:从暴力循环到O(log n)的取模写法

编程题第一道就是快速幂的变体,要求算 a 的 b 次方对 m 取模的结果,其中 b 的范围给到了 1e9 级别。如果没学过快速幂,直接写个 for 循环,对于大指数必然超时。

快速幂的核心思想是把指数拆成二进制。比如 a^13,13 的二进制是 1101,所以 a^13 = a^8 * a^4 * a^1。我们只要把底数不断平方,同时判断当前二进制位是不是1,决定要不要乘进结果,就能把 O(b) 的复杂度降到 O(log b)。

Python 写法很简洁:

def pow_mod(a, b, m): res = 1 a %= m while b > 0: if b & 1: res = res * a % m a = a * a % m b >>= 1 return res

这里有一个细节一定要提醒:进入循环前要先a %= m,否则如果 m 很大,第一步乘法就可能溢出。用 C++ 的同学更要小心,a * a在 a 接近 1e9 时已经接近 1e18,long long 还能扛住,但如果题目给的 m 更大,可能要用到快速乘或者 Python 的大整数,这也是为什么很多算法岗笔试允许用 Python 时大家都会优先选 Python。

3.2 带权并查集:食物链类型题的标准解法

第二道编程题的方向是并查集,但考的不是普通连通性,而是带权并查集,也就是节点之间不仅有"是不是同一集合"的关系,还有相对关系,比如差值、倍数、方向等。这类题的经典原型是"食物链"问题,考场上遇到的时候,很多同学当场就卡住了。

带权并查集和普通并查集相比,多维护了一个权值数组 d[x],表示节点 x 到父节点的相对值。find 的时候除了路径压缩,还要顺带更新 d[x],让 d[x] 最终表示 x 到根节点的相对值。union 的时候需要推导合并公式,这是最容易写错的地方。

我提供一个可以直接用的模板:

class WeightedUnionFind: def __init__(self, n): self.fa = list(range(n)) self.d = [0] * n def find(self, x): if self.fa[x] != x: p = self.fa[x] r = self.find(p) self.d[x] += self.d[p] self.fa[x] = r return self.fa[x] def merge(self, x, y, w): # 表示 val[x] - val[y] = w rx, ry = self.find(x), self.find(y) if rx == ry: return self.fa[rx] = ry self.d[rx] = self.d[y] - self.d[x] + w

这个公式怎么理解?合并时我们希望把 rx 接到 ry 下面,并且满足 val[x] - val[y] = w。因为 find 之后有 val[x] = d[x] + val[rx],val[y] = d[y] + val[ry],代入等式一推,d[rx] 就等于 d[y] - d[x] + w。这里符号方向依赖于题目的定义,有些题给的是差值,有些给的是模3余数关系,但模板结构是一样的。

考场上遇到带权并查集,不要慌,先根据题意确定权值数组的物理含义,再把 merge 的等式列出来,最后套模板。如果时间不够,也不建议放弃,因为并查集本身代码量不大,一旦写对,整个题目的分都能拿到。

3.3 动态规划:状态设计的三步法

第三道编程题更像是综合题,考察动态规划,数据范围区分度很明显。题目的细节我不方便透露太多,但解题思路可以讲透。动态规划题在笔试里最怕的不是转移方程写不出来,而是状态定义一开始就错了。

我自己习惯用三步法:

第一步,确认状态是什么。常见套路是dp[i]表示以 i 结尾的最优值,或者dp[i][j]表示两个序列前 i 个和前 j 个之间的最优值。状态定义一定要包含所有会影响后续决策的信息。

第二步,从"最后一步"入手推转移方程。比如编辑距离问题,dp[i][j]表示串A前 i 个字符变成串B前 j 个字符的最小编辑距离,最后一步无非是增、删、改三种操作,分别对应dp[i][j-1]+1dp[i-1][j]+1dp[i-1][j-1]+cost,取最小值就行。

第三步,确认初始化和遍历顺序。很多DP写错都是因为初始化不对,或者遍历方向反了。这个在写代码之前就要想清楚,不要编译报错后再去猜。

如果题目数据范围达到 1e5,O(n²) 的 DP 基本会被卡,这个时候就要想优化。比如最长上升子序列,O(n²) 可以写,但 1e5 的数据必须用贪心加二分,维护 tails 数组,tails[k]表示长度为 k+1 的上升子序列末尾元素的最小值。

import bisect def length_of_lis(nums): tails = [] for x in nums: idx = bisect.bisect_left(tails, x) if idx == len(tails): tails.append(x) else: tails[idx] = x return len(tails)

DP题不是看会的,是真的要动手推、动手写。考前建议把最长上升子序列、最长公共子序列、编辑距离、背包问题、区间DP、树形DP这六类经典题各练五道以上,笔试遇到基本都能套上。

3.4 堆排序与优先队列:TopK 和二分答案的配合

后面还有一道编程题考到了堆,结合了 TopK 和二分答案。题目大意是给一个数组,要求做若干次合并操作,每次取最大的两个数合并,类似哈夫曼的思路,问最终的最小代价。这类题最自然的解法就是用优先队列(堆)。

为什么用堆?因为我们需要动态维护一组数据中的最大值或最小值,每次操作只改变少数几个元素。如果用有序数组,插入是 O(n) 的;用堆,插入和弹出都是 O(log n),整体复杂度能压到 O(n log n)。

Python 里直接用heapq模块,但要注意默认是小根堆,想取最大值就把元素取负号放进去。代码大概是:

import heapq def min_cost(nums): heap = [-x for x in nums] heapq.heapify(heap) cost = 0 while len(heap) > 1: a = -heapq.heappop(heap) b = -heapq.heappop(heap) cost += a + b heapq.heappush(heap, -(a + b)) return cost

除了这类合并题,堆在"找第K大元素"里也很常见:维护一个大小为 K 的小根堆,遍历数组时如果新元素比堆顶大,就弹出堆顶、插入新元素,最后堆顶就是第K大。这样做时间复杂度 O(n log K),空间 O(K),适合海量数据场景。

还有一种高频组合是二分答案加贪心:题目问"最小化最大值"或"最大化最小值"时,先写一个判断函数,判断某个 limit 是否可行,然后二分查找答案。比如"把数组分成 m 段,让每段和的最大值最小",判断函数就用贪心去切段:

def can_split(nums, m, limit): cnt = 1 cur = 0 for x in nums: if cur + x > limit: cnt += 1 cur = x if cnt > m: return False else: cur += x return True def split_array(nums, m): lo, hi = max(nums), sum(nums) while lo < hi: mid = (lo + hi) // 2 if can_split(nums, m, mid): hi = mid else: lo = mid + 1 return lo

这个技巧很值得掌握,因为很多看着像DP的题目,用"二分答案+贪心判断"反而更简单,而且代码不容易写错。笔试时如果你感觉 DP 转移不好推,先试着往二分答案方向想一下,很多时候会有惊喜。

4. 机器学习附加题:算法岗笔试的隐藏分

4.1 KL散度与ELBO:变分推断的基础推导

附加题里有一道和变分推断相关,关键词就是最近搜索热词里频繁出现的"KL散度"和"ELBO"。这道题放在算法岗笔试里非常合理,因为现在做搜索、推荐、广告、生成模型的算法工程师,多少都会接触到变分自编码器或者扩散模型的训练目标。

先把核心公式摆出来。假设真实后验 p(z|x) 不好算,我们用 q(z) 去逼近它。KL散度的定义是:

KL(q(z) || p(z|x)) = ∫ q(z) log (q(z) / p(z|x)) dz

两边同时加上 log p(x),经过一步简单的变换可以写成:

log p(x) = ELBO + KL(q(z) || p(z|x))

其中 ELBO 的完整形式是:

ELBO = E_{q(z)}[log p(x, z)] - E_{q(z)}[log q(z)]

为什么需要 ELBO?因为 log p(x) 是证据,但直接算 KL(q(z) || p(z|x)) 需要知道真实后验 p(z|x),这本来就是算不出来的。而 ELBO 只依赖联合分布 p(x, z) 和 q(z),这两者都是可计算的。由于 KL 散度非负,ELBO 是 log p(x) 的下界,所以最大化 ELBO 就是在间接最小化 KL 散度,让 q(z) 尽量贴近真实后验。变分自编码器的损失函数本质上就是负的 ELBO,重构项加 KL 正则项。

笔试答这道题时,不用写多长的推导,但一定要把"ELBO是证据下界"和"最大化ELBO等价于最小化KL散度"这两句核心说清楚,再写一下公式,基本就能拿分。

4.2 过拟合、正则化与优化器选择

附加题后半部分考的是模型训练常识,都是选择题,题目不难,但覆盖面很广。我记得有"训练损失下降但验证损失上升,说明什么",这是典型的过拟合;还有 L1 正则和 L2 正则的区别,以及 Adam 和 SGD 的对比。

这类题要想拿分,核心理解几个点:

  • 过拟合的典型表现是训练集指标好、验证集指标差,解决手段有增加数据、降低模型复杂度、加正则化、加 Dropout、早停等。
  • L1 正则化能让一部分权重变成0,产生稀疏解,适合做特征选择;L2 正则化把权重压向0但不至于为0,对离群点更平滑。
  • Adam 自适应学习率、收敛快、对学习率不敏感,适合快速试错;SGD 加 Momentum 在某些任务上泛化性更好,但调参成本更高。现在大模型预训练里也常用 AdamW,这是 Adam 的改进版,很多笔试会作为加分项问到,知道就好。

还有梯度消失和梯度爆炸,简单说就是网络层数深了之后,梯度连乘导致太小或太大。解决方案包括BatchNorm、残差连接、梯度裁剪、合理的激活函数选择等。

4.3 方向性开放题:粒子群、卡尔曼滤波等扩展考点

最后一道附加题给了几个方向性的话题,其中包括粒子群算法、模拟退火、卡尔曼滤波、PID控制等,但不需要全答,选一个你熟悉的展开就行。这类题明显是为不同方向的算法岗位准备的,比如你做控制或自动驾驶方向,可能就选卡尔曼滤波或PID;你做优化方向,就选粒子群或模拟退火。

我当时选了粒子群算法,因为之前做过一个参数调优的小项目。回答时我讲了粒子群的核心思想:每个解是一只粒子,有位置和速度,不断向个体历史最优和群体历史最优移动,通过惯性权重、认知系数、社会系数三个参数来控制搜索行为。这种题不要求你把公式背得一字不差,但一定得能说清楚"这个算法解决什么问题、为什么有效、主要参数有哪些、和梯度类优化方法相比优缺点是什么"。

如果你投的算法岗偏传统优化,考前最好把粒子群、模拟退火、遗传算法、蚁群算法这些启发式算法的思路过一遍,每个算法准备一段两分钟能讲完的总结,笔试和面试都能用上。

5. 考后三天我在复盘什么:翻车点与笔试避坑清单

5.1 我在这场笔试里翻过的三个车

第一,KMP next 数组定义没看仔细。我把题目里的 next[i] 按前缀函数来算,但题目给的其实是失配跳转版本,结果答案完全对不上。复盘时发现选择题题干里明明写了"next[i] 定义为失配时跳转的位置",只是我一看是 KMP 就条件反射用了自己最熟的版本。这个教训太深刻了,以后拿到题先找括号里的定义,再动笔。

第二,快速幂的输入取模。我第一遍写的时候忘了a %= m,自己造了一个 m 很大、a 也很大的测试数据,计算过程直接溢出,后面修了半天。小细节决定成败,这种错误在笔试中非常可惜,因为思路完全正确,只是少了最前面那一行。

第三,带权并查集的合并公式符号写反了。我当时想着d[rx] = d[x] + d[y] + w,结果样例一直过不去。后来冷静下来,按"合并后要满足 val[x] - val[y] = w"这个条件重新推了一遍才改对。所以带权并查集一定要把等量关系写清楚,不要凭记忆背公式,我上面那个模板是用 val[x] - val[y] = w 的方向,如果题目方向相反,记得把 w 的符号换一下。

5.2 时间分配与代码风格的自检清单

这次笔试我最庆幸的是提前规划了时间,不然附加题根本没时间写。我的时间分配大概是:选择题和填空题控制在35分钟以内,不会的先标记跳过,别恋战;编程题每题留30到40分钟,按"自己最熟练的题先做"的顺序排序;最后留20分钟给机器学习附加题和整体检查。

代码风格方面,笔试环境通常不能运行调试,所以代码必须一遍写对。我给自己定的自检清单是:变量名用有意义的名字,不要写 a、b、c 满天飞;循环边界写之前先想清楚是< n还是<= n;所有数组下标从0开始还是从1开始,全程保持一致;涉及取模的地方检查每一步是否都取模了;写完在脑子里跑一个最小样例、一个空样例、一个边界样例。

尤其要注意评测机的边界条件,比如数组长度为0、n等于1、数据量达到上限的情况。很多时候不是思路错,是边界没处理完。

5.3 后续备考我调整了什么

考完这次笔试,我把后面的复习计划重新排了一版,重点从"刷题量"转向"知识结构化"。

第一,高频考点做专项整理。字符串、排序、贪心、DP、图论、并查集、快速幂,每个方向整理出一页A4纸的笔记,包含典型题型、核心公式、易错点。比如把KMP的前缀函数和失配数组两种定义分别列出来,下次再遇到直接对号入座。

第二,每天限时做两道编程题,严格按笔试节奏来,一道题最多40分钟,超时就去看题解然后复盘。不要再一题磨一小时,考场上根本不允许。

第三,机器学习基础公式全部手推一遍。KL散度、ELBO、交叉熵、SVM对偶、朴素贝叶斯、逻辑回归的损失函数,这些都是算法岗笔试和面试反复出现的内容。只记住结论不够,必须能自己推出来。

第四,每周做一次模拟笔试。用往年真题或者热门题库里的混合套题,按真实笔试的时间和题量来,训练自己在限时环境下的心态和策略。模拟笔试最大的价值不是押题,而是让你知道每类题应该花多少时间,遇到不会的题能不能果断跳过。

如果你也在准备算法岗笔试,我最大的体会就一句话:别把时间浪费在刷偏题怪题上,把KMP、排序、贪心、DP、并查集、快速幂这些常客练到条件反射,机器学习的基础公式能自己推一遍,笔试基本就稳了。最后再分享一个小技巧,考试时把题目里的数据范围圈出来再决定用什么算法,这个习惯能帮你避开很多复杂度超纲的隐患。后面如果有二面、三面的经历,我再回来继续写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询