去年整理应聘记录的时候,翻到了美团2020校招算法工程师方向笔试题的复盘笔记。当时考前刷了一个多月剑指Offer和LeetCode,觉得自己已经“算法自由”了,结果这套题做完出了一身冷汗。它不考那种一眼就能套模板的题目,反而在基础概念的变体、边界条件、工程思维上连环设卡,真正把“背题党”和“理解派”区分开。如果你正在准备大厂算法岗校招,或者想检验一下自己的算法底子,这篇文章里的复盘思路和拆解方法应该能帮你少走不少弯路。
1. 这套笔试题的画像:题型分布与出题逻辑
1.1 客观题:不背公式,考的是理解
美团这套题的客观题部分,覆盖了算法与数据结构、机器学习基础、概率统计几个大板块。给我的第一感觉是:它不直接问“快排的时间复杂度是多少”这种背概念题,而是给一个具体场景,让你判断应该选用哪种排序策略;或者给你一段有bug的代码,让你判断哪里会导致死循环;又或者给一个模型训练曲线,让你判断当前是欠拟合还是过拟合。
这种出题方式的好处是,能够筛掉单纯背题库的人。比如它考KMP时,不会只问“KMP算法的思想是什么”,而是会给你模式串p="abacaba",让你算next数组,或者判断匹配过程中模式串指针的具体跳转位置。如果你只是背了模板,换一个定义方式就很容易翻车。我在牛客讨论区看到不少同学考完吐槽:“每个知识点都见过,但选项里全是模棱两可的说法。”
另一个让我印象深刻的点是概率统计题占比不低。贝叶斯公式、期望计算、常见分布的数字特征,这些在算法岗笔试里出现频率非常高,因为做模型评估、A/B实验、用户行为分析都需要概率直觉。很多刷题导向的同学容易忽略这块,觉得“考算法嘛,把LeetCode刷好就行”,结果在客观题上栽了跟头。
1.2 编程题:算法思维与工程实现的平衡
编程题部分,重点不是考你某个库函数怎么调,而是考你能不能快速分析出问题的核心规律,然后用扎实的数据结构编码实现。印象中考察的核心方向集中在字符串处理、排序、贪心和动态规划。
很多同学在考后讨论区都说,这套编程题“看起来不难,AC起来全是坑”。原因在于它的数据范围往往会卡掉暴力解,你要么想清楚优化策略,要么得在边界条件上做到滴水不漏。比如有的题看起来就是模拟题,但数据量到10^6级别,不用二分或贪心优化就一定会超时。还有的题输出格式要求极其严格,多一个空格、少一个换行都会判错,这种细节恰恰是平时刷题不太注意的。
说白了,编程题不仅仅考算法,也考你在有限时间里把思路变成无bug代码的能力。这个能力和刷题量有关,但更和“做题时的思考方式”有关——是先想清楚再动手,还是边写边改,考场上几分钟就能看出来。
1.3 从考点分布看算法工程师需要的知识版图
整理一下这套题涉及的知识点,可以画出一张清晰的考点地图:
| 知识板块 | 具体考点 | 考察意图 |
|---|---|---|
| 数据结构 | 数组、链表、树、堆、哈希表 | 是否具备扎实的底层抽象能力 |
| 经典算法 | 排序、KMP、贪心、动态规划、二分 | 是否理解算法本质而非背模板 |
| 机器学习基础 | 聚类、分类、过拟合、交叉验证 | 是否具备建模思维和调参直觉 |
| 概率统计 | 贝叶斯、期望、分布 | 是否能和数据的不确定性打交道 |
| 工程实现 | 复杂度分析、边界条件、输入输出 | 是否能写出能上线的代码 |
这张图其实也回答了“算法工程师到底考什么”的问题。它不是让你背一堆公式,而是考察四种能力:数据结构功底、算法设计能力、模型理解深度、工程实现素养。这四块正是日常业务中做推荐、搜索、风控、定价等算法方案时最常用的底层能力。
2. 经典考点逐个拆解:KMP、排序、贪心与快速幂
2.1 KMP的next数组:搞懂失配回退才能写对
KMP是算法岗笔试和面试的常客,美团这套题对它的考察,更偏重对next数组的理解,而不是让你裸写整个匹配流程。
先明确一个最常见的问题:next数组的定义有两种约定。一种是用next[i]表示“模式串前i个字符构成的子串中,最长相等前后缀的长度”,另一种是用next[i]表示“当第i位失配时,模式串指针应该跳转到的位置下标”。这两种定义之间差一个偏移,很多考生就是死记模板,一换定义就全乱。
以模式串p="abacaba"为例,我们把每个前缀的最长相等前后缀长度列出来:
- 子串"a":0
- 子串"ab":0
- 子串"aba":1(前缀"a"=后缀"a")
- 子串"abac":0
- 子串"abaca":1("a"="a")
- 子串"abacab":2("ab"="ab")
- 子串"abacaba":3("aba"="aba")
如果用next[i]表示前i个字符的公共前后缀长度,得到的就是[0,0,0,1,0,1,2,3](下标从0开始)。如果按失配跳转位置定义,还要在这个基础上做左移和补-1的变换。搞明白这个细节,比背一百遍模板都有用,因为考题完全可能只给你一个模式串,让你写数组值。
匹配时的核心思想一句话就能说清楚:当文本串和模式串在位置j失配时,把模式串指针回退到next[j],文本串指针不回退,这样整体时间复杂度就是O(n+m)。这个“不回退文本串指针”的优化,是KMP比朴素匹配高效的关键。理解它之后,再去看代码就非常顺。KMP的应用也不只是字符串匹配,像搜索引擎的关键词匹配、敏感词过滤、生物序列比对,底层都会用到类似的前后缀思想。
2.2 排序算法:稳定性、复杂度与场景选择
排序部分,美团这套题给我的感觉是“不直接问原理,考的是应用”。比如它可能会问:给你100万个浮点数要排序,你会选哪个算法?又或者问:现在需要按年龄从小到大排序,年龄相同的人再按入职时间排序,选哪种排序算法最合适?
这种题考查的是对排序算法特性的深度理解。对比一下几个主流排序:
| 算法 | 平均复杂度 | 最坏复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(n^2) | 稳定 | 几乎有序的小数组 |
| 快速排序 | O(n log n) | O(n^2) | 不稳定 | 常规大数据量排序 |
| 归并排序 | O(n log n) | O(n log n) | 稳定 | 外部排序、稳定场景 |
| 堆排序 | O(n log n) | O(n log n) | 不稳定 | 动态取最大/最小值 |
冒泡排序虽然效率不高,但却是笔试里手写频率最高的排序算法,因为代码短、思路直白。用C++写冒泡排序时,记得加一个swapFlag,如果某一轮循环没有发生任何交换,说明数组已经有序,直接break。这个优化能把最好情况的时间复杂度降到O(n),很多考题会专门问这个点。
稳定性的意义很多人理解不到位。举个例子:先按姓名排好序,再按年龄排序,如果排序算法不稳定,第二轮的排序可能把第一轮相同年龄的人的姓名顺序打乱,结果就错了。所以问“年龄相同再按入职时间排序”这种题,答案就是归并排序,而不是快排。
我在一道题里就用到堆排序的思想:给一个实时数据流,求当前前K大元素。最优解是维护一个大小为K的小顶堆,每来一个元素就和堆顶比较,比堆顶大就替换并调整堆。这个题的考点不是堆排序代码本身,而是你能不能想到用“堆”这个数据结构来处理动态TopK问题。如果只会调sort然后取前K个,数据量一大就必然超时。
2.3 贪心与快速幂:证明能力和边界处理的试炼
贪心算法在大厂笔试里出现频率很高,因为它看起来简单,但“为什么这样贪是对的”才是拉开差距的地方。美团这套题里有一道区间调度类的变体,核心思想大家可能都听过:按结束时间排序,每次选结束最早且与已选区间不冲突的区间。
但关键不是背这个结论,而是会用交换论证法证明它是正确的。思路是这样的:假设最优解中选的第一个区间不是结束最早的区间,把它换成结束最早的区间,由于新区间的结束时间不晚于原区间,剩余可用时间不会变少,因此不会让结果变差。这个“交换不坏”的论证,是贪心算法正确性的通用证明套路。笔试里如果时间充裕,可以用反证法在草稿纸上过一遍,能帮你避开很多想当然的错误。
快速幂则是另一个高频考点。它的原理很简单:计算a^b时,不用暴力乘b次,而是不断把指数折半。写成递归公式就是:
a^b mod p = (a^(b/2))^2 * a^(b%2) mod p
用二进制来看更直观:把b展开成二进制,遍历每一位,如果当前位是1,结果就乘上当前的幂次;无论当前位是不是1,底数都要自乘一次。C++实现如下:
long long fastPow(long long a, long long b, long long p) { long long res = 1 % p; while (b > 0) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } return res; }这里有个很容易踩的坑:如果p是1,那么任何数对1取模都是0,但res初始化为1 % p时已经处理了这种情况。另一个坑是负数取模:C++里负数取模结果可能为负,如果在快速幂过程中出现(x - y) % mod这种操作,要先加mod再取模,否则答案会错得莫名其妙。这些边界细节,笔试中一旦出现,就是拉分点。
3. 拉开差距的进阶考点:启发式搜索、聚类与机器学习基础
3.1 粒子群与模拟退火:从题面到“足够好”的优化思路
美团这套题里,比较意外的是出现了一些工程优化领域常用的启发式算法概念,比如粒子群算法(PSO)和模拟退火(SA)。很多只在LeetCode上刷题的同学看到会有点懵,但这其实暴露了算法工程师岗位的真实需求:很多业务问题没有精确最优解,你需要有能力设计一个在有限时间内找到“足够好”解的方案。
粒子群算法的核心是模拟鸟群觅食。每个候选解就是一个“粒子”,它有位置和速度两个属性,每个粒子记录自己的历史最优位置pbest,整个群体记录全局最优位置gbest,每次迭代按公式更新速度和位置。速度更新公式里包含三个部分:惯性项w乘以当前速度,让粒子保持运动方向;认知项c1乘以随机数再乘(pbest - 当前位置),让粒子飞向自己的历史最佳位置;社会项c2乘以随机数再乘(gbest - 当前位置),让粒子飞向群体最佳位置。
这套算法很贴合实际场景,比如做超参数寻优时,网格搜索在参数一多就指数爆炸,粒子群则能在连续空间里快速逼近一个较优解。笔试如果考到,通常就是问你pbest和gbest的含义、速度更新公式的作用,理解了“利用群体信息共享来搜索”这个本质,就能答得八九不离十。
模拟退火则借鉴了金属退火的物理过程。它从初始解出发,每次在当前解附近生成一个新解,如果新解更优就接受;如果更差,则以概率exp(-ΔE/T)接受。T是当前温度,随着迭代温度不断下降,接受差解的概率越来越小。这个“以一定概率接受差解”的机制,就是为了跳出局部最优。这种思想在业务中的价值非常大——推荐策略调参、广告出价优化、路径规划,都可能在局部最优附近徘徊,没有模拟退火这种“偶尔走回头路”的勇气,就永远到不了全局更优解。
3.2 聚类与KNN:经典机器学习考点的底层逻辑
机器学习部分的考题,主要集中在聚类算法和KNN这类经典方法上。K-Means的流程必须烂熟于心:随机选K个中心、分配每个点到最近中心、重新计算中心、重复直到收敛。笔试常考两个点:
第一是K值怎么选。常用方法是肘部法则,画出簇内误差平方和随K变化的曲线,找到拐点;但实际问题里拐点不一定明显,这时候更重要的是结合业务判断。第二是距离度量怎么选。欧氏距离适合数值型连续特征,曼哈顿距离对噪声更鲁棒,余弦相似度适合文本和高维稀疏向量。如果不做归一化,量纲大的特征会完全主导距离计算,这是K-Means和KNN都会踩的经典坑。
KNN本身是监督学习里的懒惰算法,训练阶段基本不做事,预测时才计算样本与所有训练样本的距离,取K个最近邻投票。它原理简单,但在推荐系统、图像识别、异常检测里都有朴实用途。笔试如果考到,往往结合特征缩放这个问题:KNN距离计算对特征尺度极其敏感,所以特征归一化是必须做的预处理步骤。图像分类里面的相似图片检索,有时候用KNN思想也能做,只是特征不是原始像素,而是卷积网络提取出的向量。
这类题真正想考察的,其实是你有没有“无监督思维”。算法工程师在日常工作中经常面对没有标签的数据,怎么设计一个合理的聚类目标、怎么评估聚类效果,比背公式重要得多。比如做用户分群,你不能只看聚类轮廓系数,还要看分出来的群体在业务指标上有没有显著差异,否则聚得再漂亮也只是数字游戏。
3.3 机器学习与深度学习基础:细节决定选择题的成败
美团这类互联网大厂的算法岗笔试,机器学习基础的分量不低。我印象里考到了过拟合的解决办法、交叉验证的思路、常见激活函数的对比。
比如ReLU激活函数,它在x>0时梯度恒为1,缓解了Sigmoid的梯度消失问题,但x<0时梯度恒为0,会导致对应的神经元可能永远不更新,也就是“神经元死亡”。解决方案可以是使用Leaky ReLU,给负半轴一个很小的斜率。这些细节在看书时觉得很简单,但选择题里把好几个近似表述放在一起,很容易选错。
交叉验证里也有个容易混淆的点:什么时候用K折,什么时候用留出法。如果数据量小,K折能更充分利用数据,但计算开销大;如果数据有显著的时间顺序,比如用户行为日志,就不能随机切分,要按时间做前向验证,否则会造成数据泄漏。这个和做A/B实验时的“先看过结果再选策略”是同一个错误。
深度学习这块,不需要你手推CNN,但要明白卷积层为什么参数共享——因为图像的特征在空间上具有平移不变性,同一个卷积核可以在不同位置提取同一种局部特征;池化层为什么能下采样——因为高层特征对局部微小变化不敏感,池化能压缩尺寸并增强鲁棒性。图像分类算法从经典CNN到后来的Transformer结构,本质上都是在不同抽象层级上提取特征,理解了这一点,面对不同结构的变体题就不会慌。
3.4 工科交叉知识:PID、卡尔曼滤波背后的算法通识
除了标准算法题,美团这套题还让我看到一个现象:算法工程师的知识面需要足够广。比如控制领域常用的PID算法、状态估计里的卡尔曼滤波、工业异常检测里的相关方法,这些看着和互联网业务没什么直接关系,但它们背后的数学工具是相通的。
卡尔曼滤波本质上是最小方差意义下的最优状态估计,它融合了系统的预测值和观测值,权重由两者的不确定性决定。这和推荐系统里的点击率预估、广告投放里的效果预估,底层逻辑非常相似——都是拿先验估计和实时观测做一个最优加权。所以笔试中出现这些概念,往往不是想让你现场实现,而是想看你对“算法”的理解是不是只停留在课本层面。
说实话,我考前完全没复习PID和卡尔曼滤波,看到题目时心里是慌的。但静下来一想,这类题通常考的是概念层级:卡尔曼滤波由预测和更新两步组成、PID的比例项消除当前误差、积分项消除稳态误差、微分项抑制超调。只要平时阅读面够广,这些常识完全能答上。这件事给我最大的启发是:准备算法笔试,不要只盯着刷题平台,也要保持对交叉领域技术的泛读习惯。
4. 笔试现场的实战复盘:时间分配、踩坑记录与刷题路线
4.1 时间分配:先把该拿的分拿到手
我当时的策略是先把客观题快速过一遍,遇到拿不准的先标记,绝对不恋战。客观题的分值和耗时往往不成正比,死磕一道概率题可能浪费20分钟,最后编程题却来不及写,这种亏损没法弥补。
编程题部分,我的习惯是先读一遍所有题目,在草稿纸上给每道题标一个难度预估值,然后按“最容易AC”到“最难”的顺序做。美团这套题的编程题经常是第一题最简单、最后一题最难,这种排序本身就有引导性。我见过有同学在最后一道题上死磕,结果前面的简单题都没写完整,属于典型的战术失误。笔试和面试不一样,没有追问和解释的机会,得分才是硬道理,先把稳定能拿到的分拿到,再考虑挑战难题。
时间分配上,我一般会把总时长的前20%留给客观题,之后把大头给编程题,最后留5到10分钟检查输入输出格式和边界条件。这个比例可以根据自己的强弱项微调,但一定要预留检查时间,因为“样例过了但提交0分”这种事,大概率就是文件读写出问题。
4.2 笔试现场最容易踩的五个坑
我把踩过的坑和身边同学反馈的问题整理出来,每个都很小,但在高压环境下很容易让人崩溃。
第一个坑是输入读取。有些题目会用while(cin >> x)反复读入多组测试数据,如果用getline读取,要小心上一行末尾残留的换行符。我当时就遇到一道题,第一组样例正常,第二组开始读到的数据总是缺一位,急得满头大汗,最后发现是缓冲区里的换行没清掉。
第二个坑是快速幂的边界。底数取模之后可能为负,要先加摸再取模。这在C++里特别容易踩,因为C++的负数取模结果是负数,和数学上的定义不一样。
第三个坑是排序比较函数的严格弱序问题。如果自定义的compare函数里,两个元素相等时返回true,std::sort会触发未定义行为,直接崩溃。比较函数里相等一定要返回false。
第四个坑是KMP的next数组定义。不同教材定义不一样,有“前i个字符的最长公共前后缀长度”和“失配时跳转位置”两种,用之前必须自己心里清楚,否则匹配循环的边界很容易写错。
第五个坑是输出格式。多一个空格、少一个换行都可能判错。有些题目要求每个数字之间用空格分隔且行末不能有多余空格,这个细节平时刷题就要养成习惯。
4.3 刷题路线的取舍:吃透专题比堆数量重要
结合这套题,我给准备校招的同学一个可以复用的刷题路线。第一步,先把数组、链表、栈、队列、哈希表、树、堆这些数据结构过一遍,要求是能手写核心操作,尤其是树的遍历和堆的调整。第二步,把排序算法逐个实现一遍,重点理解稳定性和复杂度,尤其是快排和归并的代码要能闭眼写出来。第三步,按专题刷经典算法:二分、双指针、贪心、回溯、动态规划、KMP、最短路、最小生成树、并查集。第四步,补充机器学习和统计概率的基础概念,这部分不用刷题,但要能够解释清楚原理。
刷题平台用LeetCode和牛客就够,不需要把平台题库刷完。我个人更推荐“专题吃透法”:每个专题精做20道题,做完之后总结套路,比浅尝辄止刷500道有用得多。比如动态规划,你刷了20道之后会发现,大部分题都是“定义状态、写转移方程、初始化、找答案”四步,难点只是状态定义的角度不同。
另外,很多人喜欢在搜索引擎里找“算法流程图”来辅助理解,这个习惯其实很好。遇到复杂题,先在草稿纸上画出流程或者状态转移图,再动手写代码,正确率会高很多。笔试现场虽然没有搜索引擎,但草稿纸就是你的“流程图工具”,别省这一步。
我后来回头看,美团2020校招算法工程师方向的这套笔试题,本质上是想找一种人:他能把算法从“会背”变成“会想”,把代码从“能跑”变成“跑得对”。如果你做完这套题觉得哪里都差一点,不用慌,那说明你的知识体系里还有漏洞。比起焦虑,把错题对应到考点地图里逐个补齐,是这个阶段最划算的投入。