1. 从一张卷子反推:2018年算法校招想筛选什么样的人
2018年秋招那阵子,我在笔试现场拿到这份算法工程师试卷时,第一反应是“题量真大”。后来才明白,这种高密度、强覆盖的卷子不只是考你会不会,而是在用最短的时间筛掉一批人。做过几年算法岗面试后,再回头看这份试卷,它的出题逻辑其实非常清晰:基础知识要扎实,机器学习要懂原理,工程能力要过关,代码要写得出来。
很多同学在网上找到这份试卷,第一反应是到处搜答案,想把每道题背下来。但我的建议是,先把这张卷子当成一面镜子,照一照“2018年一家电商公司的算法团队想要什么样的人”,再针对性准备。本文就说清楚这份试卷背后的考察逻辑,并拆解每一类题型的核心知识点和备考方法。适合正在准备算法岗校招的同学、想转行做算法的工程师,以及带新人的算法负责人参考。
我们先把这份试卷的“骨架”还原出来。通常这类校招笔试题型分布如下:
| 考察模块 | 大致占比 | 常见题型 | 备考优先级 |
|---|---|---|---|
| 数学与基础算法 | 30%左右 | 选择、填空、简答;KMP、排序、树、图 | 极高,属于“送分题”和“基础分” |
| 机器学习与深度学习 | 30%左右 | 概念题、公式推导、场景应用 | 极高,2018年前后尤其看重手推公式 |
| 大数据与工程认知 | 15%左右 | MapReduce、Spark、特征工程、数据倾斜 | 中等,区分度较大 |
| 编程题 | 25%左右 | 在线OJ、白板编程:DP、BFS/DFS、字符串处理 | 极高,直接决定能否进入面试 |
这个结构在当时并非个例。2018年正处于深度学习全面普及、大数据技术栈走向成熟的节点,企业既希望候选人懂模型原理,又希望他们能落地到分布式环境里跑数据,所以笔试里才会同时出现“手推损失函数梯度”和“Spark中的数据倾斜怎么处理”这类横跨理论与工程的题目。换句话说,这份试卷考察的不只是知识点,而是你有没有建立一套从数据到模型再到业务的完整思维链路。
2. 基础算法题:KMP、排序与数据结构的“熟而不深”陷阱
2.1 一道KMP题就足以暴露“背答案”和“真理解”的区别
在热词列表里,有一道非常典型的题:“对于模式串 p=‘abacaba’,其 next 数组定义为……” 很多同学看到“next数组”就开始背KMP的模板。但我要提醒你,这种题恰恰是出题人用来区分“背模板”和“真理解”的。
先明确一个容易混淆的定义:在KMP算法中,next数组通常有两种定义方式。
- 一种定义是
next[i]表示模式串前 i 个字符组成的子串中,最长的相同前后缀长度。这是一个“长度值”,常用于生成“部分匹配表”。 - 另一种定义是失配后指针应该跳转到的位置,即
next[i] = 最长相同前后缀长度,但在实际代码里有的教材会做“减一偏移”或“整体右移”的处理。
如果试卷上没有特殊说明,通常考察的是第一种:next[i]表示子串p[0...i-1](即前 i 个字符)中最长相同前后缀的长度,next[0]统一取 -1 或 0。
我们来手推模式串p = "abacaba":
| 子串 | 长度 | 最长相同前后缀 | next值(按长度定义) |
|---|---|---|---|
| 空串 | 0 | 无 | next[0] = 0(或-1) |
| "a" | 1 | 无 | next[1] = 0 |
| "ab" | 2 | 无 | next[2] = 0 |
| "aba" | 3 | "a" | next[3] = 1 |
| "abac" | 4 | 无 | next[4] = 0 |
| "abaca" | 5 | "a" | next[5] = 1 |
| "abacab" | 6 | "ab" | next[6] = 2 |
| "abacaba" | 7 | "aba" | next[7] = 3 |
如果按“失配跳转位置”的常见代码写法(整体右移再补 -1),数组会变成[-1, 0, 0, 1, 0, 1, 2, 3]这类形式。题目里如果写的是“next[i] 定义为”,务必看清它的基准是长度还是跳转位置,很多考生就是在这里丢分。
如果你只是记住了KMP的代码模板,遇到“求next数组”的填空题还能应付,但遇到“为什么复杂度是O(m+n)”“为什么不能用朴素算法在字符串中查找多个模式串”这类变体就懵了。KMP的核心思想是:当发生失配时,已经匹配过的部分里,前缀和后缀有重叠,利用这个信息跳过不必要的重复比较。举个例子,匹配到"abacab"时发现下一个字符不匹配,此时不需要回到模式串开头重新比较,因为"ab"已经在当前文本的尾部出现过,直接从模式串的第3个字符(即'a'后面)开始继续比较即可。
我在实际业务里用到KMP的场景不算多,但它作为“理解字符串匹配本质”的入门题,在校招笔试中出现的频率极高。备考时不要只背模板,找一个断点自己动手画一遍匹配过程,比刷十道同类题都管用。
2.2 排序算法:不只是写出来,还要知道为什么选它
每个算法岗位的笔试卷里,排序算法都会反复出现。热词列表里无论是“冒泡排序算法c++”“堆排序算法”还是“快速幂算法c++”,本质上都在提醒你:基础算法的手写能力是绕不开的。
排序算法题通常有以下几种考法:
- 手写快排、归并、堆排的完整代码;
- 给出一个序列,写出某排序算法每一趟的结果;
- 问某个排序算法是否稳定、最好/最坏/平均时间复杂度;
- 在特定场景下选排序方案,如“内存不足,如何对1TB文件中的整数排序”。
这里给出常见的排序算法对比,方便快速回顾:
| 排序算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 教学演示,基本不用 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 | 通用排序,C++ std::sort的底层基础 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 | 外部排序、链表排序 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 | TopK、优先队列 |
笔试中经常出现的场景题是“从海量数据中取最大的K个数”。最直接的方案是用堆:维护一个大小为K的小顶堆,每来一个数就与堆顶比较,大于堆顶就替换并调整堆。时间复杂度为 O(n log K),而全排序需要 O(n log n),当 n 远大于 K 时差距非常明显。如果K也很大的情况,可以考虑先用位图或者分桶预处理,再在每个桶内排序汇总。
我在带人时发现一个共性问题:很多人能写出快排,但说不清“为什么快排平均是O(n log n),而最坏会退化到O(n²)”。答案是分区点选择的随机性问题——如果每次选到的是最小/最大值,分区就会退化成一边倒,递归深度变成n。这也是为什么工程实现里会引入“三数取中”或者“随机化”来避免最坏情况。这类“为什么”比“怎么写”更能拉分。
2.3 图论与贪心:高频中的高频
热词列表里还有一批图论和优化类算法:Dijkstra、二分图HK算法、Kahn算法、贪心算法、模拟退火、粒子群、剪枝等。在校招笔试中,Dijkstra和贪心是出现频率最高的。
Dijkstra考察的多是“在非负权图中求单源最短路径”。注意它的前提条件:图中不能有负权边。如果面试官追问“为什么”,你要能说出原因是它依赖“当前最短路径不会再被更新”这一性质,而负权边会破坏这个假设。遇到有负权边的场景应该用Bellman-Ford或者SPFA。多数人只记住了堆优化的Dijkstra模板,却忽略了前提条件,这在笔试和面试中都容易被“挂掉”。
贪心算法的难点在于证明“贪心策略为什么是对的”。典型的例子是“活动选择问题”:给定若干区间,选出最多不重叠的区间,贪心策略是按结束时间排序,依次选择不冲突的区间。这类题想拿满分,不仅要答出贪心策略,还要说明为什么这样的局部最优能推出全局最优。在笔试卷里,这类题往往以“设计一个算法并证明”的形式出现。
我的建议是,基础算法部分的复习以“理解原理 + 手写实现 + 能解释复杂度”为准,不要只追求刷题数量。因为校招笔试考的是“你会不会”,而面试考的是“你懂不懂”,两份卷子的底层数据结构其实是同一套。
3. 机器学习与深度学习题:背概念看似会,手推公式才见真章
3.1 KNN这类模型题,可以从三个维度切入作答
热词里有“knn算法的应用能力包括哪三个方面”,这道题非常典型。KNN(K近邻)是最基础的监督学习算法之一,它的“应用能力”可以从分类、回归、以及异常检测/推荐等场景来回答。
- 分类:对未知样本,通过距离度量(欧氏距离、曼哈顿距离、余弦相似度等)找出训练集中最近的K个样本,投票决定类别。
- 回归:同样找K个近邻,但输出是这K个样本标签的平均值或加权平均值。
- 异常检测/推荐:当某个样本与其K个近邻的平均距离很大时,可以认为是异常点;推荐系统里也可以用“与用户行为最相似的K个用户”来做协同过滤。
答题时如果只写“KNN用于分类”就太单薄了。可以继续补充:KNN是“惰性学习”(lazy learning),训练阶段不学参数,预测阶段才计算距离,因此预测耗时和样本量成正比,不适合高延迟要求的在线场景。同时它在高维空间中会遇到“维度灾难”——距离度量会变得不再有效,这也是为什么在高维数据上常常先做降维再使用KNN。
在校招笔试里,这类概念题不需要写长篇大论文,但要求你能把关键点答全,并且能用一个小的例子把逻辑串起来。最好的方式是先在草稿纸上写一个三步结构:是什么、怎么用、有什么局限。
3.2 “KL ELBO”这类推导题:考的是变分推断的逻辑链条
热词里出现了“kl elbo 算法原理详解”,这其实是变分推断(Variational Inference)里的核心:KL散度与ELBO(Evidence Lower Bound,证据下界)的关系。
先给一个简单版本的理解:我们希望用分布 q(z) 去近似后验分布 p(z|x)。直接优化KL散度 KL(q(z) || p(z|x)) 很困难,因为 p(z|x) 本身是未知的。于是我们转而最大化ELBO,因为它等价于“最小化KL散度 + 常数项”。
ELBO写出来是:
ELBO(q) = E_{q(z)}[log p(x, z)] - E_{q(z)}[log q(z)]
或者等价形式:
ELBO(q) = E_{q(z)}[log p(x|z)] - KL(q(z) || p(z))
这里的第二项是“近似分布与先验分布的KL散度”,第一项是“重建似然”。优化ELBO就是在“让模型更好地解释数据”和“让近似后验不要偏离先验太远”之间做权衡。
2018年前后,这类偏生成模型和贝叶斯方法的推导题出现频率并不低。如果你报的岗位偏向NLP或推荐方向,甚至面试官会直接让你在白板上推一版简易VAE的损失函数。VAE的损失函数其实就是“重建损失 + KL项”,只不过把高斯假设代入后,KL散度有了闭合形式的解。你不需要把每个公式背得一字不差,但要清楚每一步的数学依据,以及每个符号的含义。
我理解很多同学觉得“手推公式”是笔试里最痛苦的部分,但换个角度想:面试官并不是要你当一个计算器,而是通过公式推导的过程,判断你有没有真正理解这个算法。如果一个人能从头到尾推导出一个逻辑回归的梯度更新过程,那他在实际调参时一定比只会调库的人更有方向感。
3.3 深度学习与经典模型:2018年的考察重心
在2018年的笔试里,深度学习的比重明显上升。卷积神经网络、循环神经网络、注意力机制、反向传播的链式法则,几乎每家公司都会考到。与此同时,当年最火的XGBoost也在题目里频繁出现,因为这是传统机器学习落地的巅峰期。
一道典型的考题是“解释随机森林与GBDT的差异”。可以从以下几个方面回答:
- 随机森林是Bagging思路,训练多棵决策树,每棵树独立训练,最后投票或平均;GBDT是Boosting思路,每棵树拟合前一轮的残差。
- 随机森林的基学习器可以并行训练;GBDT必须串行,但通常用更浅的树,且能处理更复杂的非线性关系。
- 随机森林对异常值更鲁棒;GBDT对异常值较敏感,因为拟合残差时异常点会产生很大损失。
如果题目里再加一层难度,会问“XGBoost相比GBDT做了哪些改进”,那就需要提到二阶泰勒展开、正则项、列采样、近似分位数分裂等。这些细节在2018年的面经里被反复讨论,说明出题人非常看重候选人是否读过原始论文,而不只是看过一篇博客。
备考机器学习部分,我的建议是“以模型为纲,以公式为线”。对每一个经典模型,至少能回答三件事:假设是什么、损失函数怎么写、优化怎么更新。把这个框架整理成自己的笔记,笔试时遇到任何模型题,都会有一根主线可以顺下来,而不是东一句西一句拼凑。
4. 大数据与工程认知题:这张试卷里最具时代感的那部分
4.1 Spark、MapReduce和“数据倾斜”为什么会在算法卷里出现
在很多算法岗笔试中,大数据模块往往被同学们忽视,但它在区分度高的时候恰恰是决定你能不能进面试的关键。2018年的背景是Spark已经很普及,但MapReduce的思想仍然是考察大数据认知的起点。
一道经典题目是“用MapReduce实现WordCount”。看似简单,但考察点在于:
- Map阶段:把文本中的每个单词映射为
<word, 1>; - Shuffle阶段:按word做分区和排序,把相同的key聚到同一个reducer;
- Reduce阶段:对同一key的value列表求和。
如果你能进一步指出“为什么Shuffle是MapReduce最昂贵的阶段”“如何在Map端做Combine以减少网络传输量”,这道题就能从及格变成高分。
另一个高频考点是“数据倾斜”。常见场景:join操作中某个key的数据量极大,导致某个reduce任务成为长尾任务;或者group by中某些key特别集中。解决方案不外乎以下几种:
- 增大reduce任务数量,让负载更分散,但这治标不治本;
- 对热点key做加盐处理,比如拆成多个子key,再汇总;
- 两阶段聚合:本地聚合一次,全局聚合一次;
- 广播小表,避免大表join小表的Shuffle。
算法工程师在实际工作中,数据清洗、特征拼接、模型训练样本生成,每一步都可能遇到数据倾斜。笔试考这个,是因为公司希望招进来的人不是“只会写模型训练脚本”,而是能处理真实数据管道问题的人。
4.2 特征工程与模型上线:工程思维是隐藏考点
除了分布式计算,特征工程也是试卷中反复出现的“软考点”。比如题目问“特征标准化对哪些模型影响大,对哪些模型影响小”。答案分两类:
- 对基于距离/梯度的模型(KNN、SVM、逻辑回归、神经网络)影响大。因为特征尺度差异会导致梯度更新方向偏向大尺度特征,距离计算也会被大数值特征主导。
- 对基于树的模型(决策树、随机森林、GBDT、XGBoost)影响小。因为树模型在分裂时只找切分点,不依赖特征的具体数值比例。
如果在回答时还能补充一句“在深度学习中,特征标准化还能帮助优化器更好地收敛,甚至在BatchNorm出现后,内部协变量偏移问题得到了缓解”,那这道题就答出了深度。
还有一类工程题是“模型离线效果好,线上效果差,怎么排查”。这类题的答题框架是:
- 离线与在线的特征一致性是否保证(训练样本和线上预测样本是否同分布);
- 样本选择是否有偏,比如只采样了活跃用户;
- 数据时间窗口是否过期,模型是否定期更新;
- 线上特征缺失时的默认值处理是否与训练一致。
这些内容在2018年的校招卷子里并不是必考,但在面试中问到的概率极高。因为笔试能筛出“懂理论”的人,面试才能筛出“能干活”的人。而“能干活”的核心,就是具备工程闭环思维。
4.3 排序算法与外部排序:把“基础”和“工程”串起来
在热词列表里,“数据结构排序算法”和“堆排序算法”反复出现。很多人以为排序只考手写代码,其实还有一个更高级的考法和工程场景强相关:外部排序。
如果内存不足以容纳所有数据,比如要对1TB的整数排序,怎么做?经典的方案是“外部归并排序”:
- 把大文件切分成若干能装进内存的块,对每块用快排排序后写回磁盘,产生多个有序子文件;
- 用败者树或堆,从每个有序子文件中取当前最小值,归并成一个更大的有序文件;
- 重复上述归并,直到只剩一个文件。
这个方案里就用到第2.2节说的堆。堆排序不仅是面试题,更是外部排序和优先队列的底层实现。所以我在复习时总会提醒自己:不要孤立地看待知识点,笔试里那些看起来毫无关联的“堆排序”“KMP”“MapReduce”,在真实业务里往往会通过某种方式连成一条线。
当年我在这部分吃亏过:以为算法工程师笔试就是写模型、调参数,结果遇到“内存限制下做TopK”时,第一反应是“用Spark”。但笔试环境里没有Spark,只有白板和时间。那之后我把基础知识从头过了一遍,才对“算法工程师”这个岗位有了更全面的认识。
5. 在线编程题:从暴力枚举到AC的临场节奏
5.1 看清在线评测平台的“脾气”:IO是第一步
算法岗笔试的最后一关通常是编程题。很多人理论题答得不错,编程题却因为环境问题丢了分。这里说的“环境问题”不是电脑问题,而是不熟悉评测平台的输入输出、内存限制和编译规则。
常见平台采用“标准输入输出”方式,要求从stdin读取,结果输出到stdout。也就是说,你不仅要写出核心算法,还要处理输入解析。比如一次给多行数据,每行有多个整数,你需要根据第一行的n决定后面读几行;或者输入带有逗号分隔的字符串,你要先split再转int。这些操作看似简单,但非常容易在紧张状态下出错。
我的建议是:进入笔试前,先把目标平台的“输入输出Demo题”刷一遍,确保自己熟悉while (cin >> n)这种C++写法,或者Python的sys.stdin.read().split()套路。不要等到考场上才第一次写带IO的代码。
5.2 拿到题目别急着“最优解”,先有“暴力解”
在线编程题的时间通常是40到60分钟做2到3道题。很多人犯的错误是一上来就想着最优解,结果卡在中间下不了笔,最后连暴力分都没拿到。我的经验是,先确定暴力解法能拿多少分,再逐步优化。
比如一道典型的动态规划题“最长上升子序列”。最经典的DP解法是 O(n²) 的:定义 dp[i] 表示以第 i 个元素结尾的最长上升子序列长度,遍历 i 之前的所有元素更新 dp[i]。如果数据范围 n <= 1000,这个解法完全足够。如果 n <= 100000,则需要用“贪心 + 二分”的 O(n log n) 解法。
笔试时你要快速判断数据范围。如果n很大,直接写O(n²)很可能超时,但如果没有更好的思路,写出暴力和部分DP也能获得部分通过分数。很多OJ采用“按通过的数据点给分”机制,部分通过比交白卷强太多。
5.3 临场节奏的取舍:学会“战略性放弃”
编程题往往不止一道,份量和难度也不同。我见过不少同学在第一道题上死磕了40分钟,结果后两道题连题目都没读完。正确的节奏应该是:
- 用2到3分钟读完所有题目,判断难度梯度;
- 从最简单、最有把握的一道开始写;
- 每道题在写完暴力版本后,如果时间允许,再考虑优化;
- 如果某道题卡了15分钟以上没有进展,立刻标记并转向下一题。
这种“先易后难、写不完也要留痕”的做法,听起来很简单,但在限时状态下特别考验心态。你可以从现在开始就用OJ做模拟考试,严格要求自己按这个节奏走,把临场感提前练出来。
我自己每次模拟笔试时,还会刻意留5分钟回看代码,检查数组边界、是否溢出、输出格式是否完全匹配。很多“差一点AC”的题,问题往往就出在i++写成了++i,或者循环边界少了一个等号。代码写得对不对,靠的是习惯,考试时根本没有时间让你复查太多遍。
6. 当年没答好的三道题,如今回头看全是经验
6.1 第一道:KMP的 next 数组,背错了定义
2018年我自己做类似卷子时,KMP这一题也曾经被卡住。不是不会求next数组,而是我背的那套模板和题目里给出的定义不一致。题目里next[i]是按“长度”定义的,我下意识按“跳转位置”写,结果整道题从第一个空开始就错了。
这件事让我养成了一个习惯:任何算法题目,先看清楚“定义是什么”再动手。哪怕是背模板背得再熟,也要把题目里的变量含义、边界条件先标注出来。这个习惯不仅在校招笔试里有用,在后来阅读开源代码、排查线上问题时也同样关键。
6.2 第二道:手推LR梯度,卡在链式法则
机器学习部分我当年最怕的就是“手推逻辑回归的梯度”。其实逻辑回归的推导很简单:先写出似然函数,再取负对数得到损失函数,然后对参数求偏导。求导过程中需要用到sigmoid' = sigmoid * (1 - sigmoid)这个性质,最终梯度是(预测值 - 标签) * 特征。
但考试时我会慌,因为平时都是直接调sklearn的LogisticRegression,没有自己推过一遍。后来我花了几天时间,把所有常用模型的推导都从头写了一遍,包括线性回归最小二乘法、逻辑回归、朴素贝叶斯、SVM的对偶问题,甚至把CNN里一层卷积的反向传播手算了。这个过程很枯燥,但效果立竿见影——之后再遇到推导题,我不需要背,因为每一步都能从“为什么要这样算”想明白。
6.3 第三道:编程题只写了暴力解,但拿到了大部分分
有一道编程题,我至今记得特别清楚,是道字符串相关的题目,第一反应是用动态规划,但我没想清楚状态定义,于是先写了一个暴力枚举所有子串的版本。写完暴力版本后,我突然想明白状态转移了,于是改成DP解法,最终AC了。
如果当时我因为“暴力解太low”而不肯写,可能连验算的机会都没有,更别提靠着暴力版本的时间把思路理顺。这个经历后来被我反复讲给团队里的新人:代码能力不是“一步到位”,而是“先跑起来,再变快”。笔试本身就是一场工程实践,谁能在有限资源下给出可行解,谁就赢了一半。
6.4 从备考到实战:这套复习框架如今依然适用
每次有人问我“算法校招怎么准备”,我都会给出一个很朴素的答案:不要迷信刷题量,而要把每个知识点拆成“是什么、为什么、怎么用”三层。用这份试卷来打比方,你要想的不只是“KMP怎么写”,而是“为什么KMP能线性匹配”“它在哪些场景下能派上用场”“换一道字符串题我能不能想到类似思路”。
如果你想系统训练,可以按这个顺序来:
- 第1到2周:过一遍基础数据结构与排序,手写所有经典排序,理解复杂度;
- 第3到4周:集中刷KMP、Dijkstra、DP、贪心、回溯,重点是分类总结;
- 第5到6周:系统整理机器学习经典模型的公式推导,配合面经做查漏补缺;
- 第7周:专项补大数据和工程知识,特别是Spark和特征工程;
- 第8周:做3到5次全真模拟笔试,严格限时,训练临场节奏。
如果你正在准备算法岗校招,可以把一份旧试卷当成镜子,而不是题库。镜子照出来的,是你对算法工程师这个岗位的理解是否完整;题库照出来的,只是你还有多少题没刷过。这两种思路,最终会把你带到完全不同的高度。