每年三到五月,互联网行业的实习生招聘都会进入一个密集出题期。2017年的阿里春招也不例外,第二场笔试(题集二)当时在应届生里讨论度很高。它和第一批次题目最明显的区别在于——不是更难,而是更“碎”:客观题覆盖面广,编程题难度梯度拉得比较开,最后还有一道选做附加题。很多同学把精力全压在算法上,最后反而在客观题环节丢掉太多分。这篇文章不打算只给一套答案,而是从出题逻辑、题型分析、做题顺序到判题环境的完整复盘,帮各位准备一线大厂实习笔试时少走弯路。
1. 当年那场笔试到底考了什么:先看清试卷结构再谈刷题
1.1 为什么说实习生笔试拼的并不是算法天花板
我见过不少准备实习笔试的同学,一上来就钻进LeetCode的困难题里,觉得只有做难题才能进大厂。但等到真的上了考场,你会发现实习生笔试筛选的核心指标根本不是“你会不会难题”,而是“你在有限时间内能不能稳定输出基础能力”。
2017年阿里实习生笔试(二)的题目体系,我根据当时参加的同学反馈和自己整理的题目印象,整体是这样分布的:单选题覆盖数据结构、网络、操作系统、Java/C++基础;多选题主要考概念辨析和边界细节;简答题会问你某个场景下会选用什么技术方案;最后两道到三道编程题,难度从签到题到中等偏上不等。附加题通常不强制完成,但做出来会有额外加分。
这个结构说明一个问题:出题人默认你是来实习的,不是来参加算法竞赛的。实习生进组之后,要先能在业务代码里看懂逻辑、能处理边界情况、能在别人留下的代码基础上快速改需求。所以笔试里会大量出现“场景题”,把知识点包装成一段业务描述,问你哪个方案最合理。单纯会背“红黑树查找时间复杂度是O(log n)”没用,你得知道在百万级数据、内存有限的场景下选它是不是最优解。
还有一个容易被忽略的点:这套题的题量并不小,时间却只给了一到两小时。很多人不是不会做,而是被前面的客观题纠缠太久,到编程题时只剩十几分钟。这也是我特别想强调的:实习生笔试拼的不是算法天花板,而是时间分配和稳定拿分的能力。
1.2 从题型分布反推复习顺序:客观题、简答题、编程题各有各的坑
如果按“丢分惨烈程度”来给这三类题型排个序,我的感受是:多选题大于编程题,编程题大于单选题。多选题的规则通常很残忍,少选、多选、错选都不得分,甚至有些平台是选错一个选项整题零分。这种规则逼着你在考场上有一种“克制感”,不能像做单选题一样凭感觉猛选。
我当时一个比较深的印象是,简答题并不需要你写长篇大论,它就是问你“如果要设计一个短链接系统,你会分成哪几个模块”,或者“线上服务出现CPU飙升,你从哪些角度排查”。这类题目没有标准代码,更像是在考察工程直觉。如果你只是埋头刷算法,不接触真实项目,到这种题目上会明显感觉到无话可说。
编程题里的坑则体现在“看起来简单,样例也过了,一提交却超时”。原因无非是复杂度估错了,或者没处理数据范围。2017年这套笔试题里,编程题输入规模的上限写得比较夸张,有些题故意放在最后,就是考验你能不能快速判断出该用哪种算法。如果一上来就写递归,大概率会在大数据量上爆栈。
所以复习顺序上,我建议这样安排:先用两到三天把数据结构和操作系统的基础概念过一遍,边看边配选择题练习,把客观题的准确率提上去;再花更多时间刷中等难度的算法题,每道题都要做复杂度分析,不能AC了就扔;最后再去找一些开放性的设计题、场景题来练手,提升自己用自然语言描述方案的能力。这个顺序比“一天刷十道难题”要稳得多。
2. 客观题丢分重灾区:基础概念越熟悉越容易错
2.1 数据结构题:会“用”不等于会“选”
按我对这套笔试的印象,数据结构相关的选择题很少直接问“栈的特点是什么”,更多的做法是给你一个具体场景,让你在几个结构之间做选择。这种题最容易翻车的地方在于,每个选项单独看都有一点道理,但你要选出的是“最优解”。
比如有一类很典型的问题:现在需要频繁按某个关键字查找记录,数据总量会动态增长,你会选择哪种存储结构?选项里给出顺序表、二叉搜索树、哈希表、跳表。很多人看到“查找”就直接选哈希表,却忽略了“动态增长”这个前置条件。如果数据量增长频繁,哈希表可能面临多次扩容和rehash,而跳表在插入和查找之间能保持一个更平稳的性能。这类题考的不是你会不会用哈希表,而是你能不能把“最差情况”和“平均情况”分开考虑。
还有一个高频方向是链表和数组的区别。笔试里会把它包装成“一段内存中需要频繁在头部插入和删除元素,用哪种结构更好”。你要是只记得“链表插入删除快”,就会漏掉一个前提:题目可能在后面补了一句“内存空间要求尽量连续”,那答案就要倾向数组加标记位,或者用带空闲链表的数组。出题人不会无聊到故意难为你,他是在模拟真实工程里的取舍。
我在做这类题时的一个习惯是:把题干里的每个限定词都圈出来,比如“频繁”“平均”“最坏”“内存受限”“数据有序”。这几个词基本上决定了答案。如果你读完题只看到“查找”两个字,后面的条件全被忽略,那丢掉的分不冤枉。
2.2 操作系统和网络:经常以“二选一”形式出现
操作系统部分,线程和进程的区别、死锁产生的四个必要条件、虚拟内存和分页机制是固定考法。网络部分则集中在TCP三次握手、TIME_WAIT状态、HTTP和HTTPS的区别、DNS解析过程。
这套笔试里有个很有意思的现象:它不直接问“TCP和UDP有什么区别”,而是给你一份聊天应用的描述,让你判断为什么选择TCP而不是UDP。这种题大部分人都能答对,真正的陷阱在后面一题,它可能会反过来说“在线视频通话延时要低,为什么可以用UDP”,然后让多选里有“因为可以容忍少量丢包”和“因为不需要可靠传输”两个选项。猛一看都是对的,但“不需要可靠传输”这个说法放到视频场景里就不严谨,视频也需要可靠传输关键帧,只是在实现上通过应用层做了容错,而不是说可以完全不需要。
复习这块内容的时候,不要只背结论。我建议把每个协议或机制都问自己一层“为什么”。比如TCP为什么要三次握手而不是两次,TIME_WAIT为什么要保留2MSL,进程和线程切换各自的成本到底差在哪。这些“为什么”才是多选题里真正想考的东西。你背的结论只能应付单选,多选一定会在细节上让你纠结。
2.3 语言细节与数学逻辑题:考场上的秒杀技巧
这一部分我印象里包括不少C/C++和Java的语言细节题。比如“下列哪种方式能正确创建线程”,或者“Java中String、StringBuilder、StringBuffer在不同拼接场景下的效率”。这类题对项目经验少的人来说反而好拿分,因为都是固定知识点,只要背过就能答对。
最容易丢分的是位运算相关的题,比如“给定一个整数,如何判断它是2的n次幂”。代码写法很简单,一次n > 0 && (n & (n - 1)) == 0就能解决。但笔试不会让你写代码,它把四个写法放在选项里,每个看起来都很接近,只有一个是兼顾“正数判断”和“位运算正确性”的。我当时就吃过亏,以为n & (n-1) == 0就是全部,忘了正数判断。这种题考得很细,但也说明一个道理:基础知识别停留在“我见过”,要精确到“我能判断每一个边界”。
数学与逻辑题通常出现在填空题或者单选的后半段,比如条件概率、期望值、排列组合。这类题目的难度不高,但计算量有一点。我的经验是,遇到概率题不要急着列公式,先在草稿纸上画一棵树或者列几个分支,把每一种情况的概率标出来,这样不容易漏。复杂排列组合题如果超过三分钟还没有清晰思路,果断跳过去,后面的编程题更值钱。
3. 编程题从读题到AC:三道典型题的完整推导过程
3.1 字符串题:把“反转/切割”类需求翻译成边界条件
这套笔试的编程题第一题通常不会太难,但题干会写得比较绕。常见的是字符串处理,比如“给定一个字符串,按单词为单位反转,多余的空白字符需要去除,每个单词之间保留一个空格”。这类题摆在第一题,不是想卡人,而是测试基本功和细心程度。
我以这类题为例,说一下做题时的拆解流程。第一步,先把题目里的操作指令提取出来:反转、按单词切分、去除多余空格。第二步,明确边界条件:字符串为空、字符串全是空格、单词之间有多个连续空格、字符串首尾有空格。第三步才开始写代码。
很多同学的错误在于,一上来就调用语言自带的split函数分割单词,然后倒序遍历输出。这在大多数测试用例上没问题,但遇到连续多个空格时,某些语言的split行为会不一致,容易在输出里多出空白字符串。更稳的做法是手动扫描字符串,按“当前字符是否为空格”作为状态切换的条件,把单词收集到一个列表里,最后统一拼接。这样不管空格怎么变化,结果都是可控的。
我还记得当年笔试时,这类题往往有个隐藏要求:原始字符串可能长达10^6,所以不能频繁做字符串拼接,否则会产生大量临时对象导致超时。正确做法是使用可变字符数组或列表收集结果,最后一次性转成字符串。这一步是“能过样例”和“能通过全部大数据用例”的区别。
3.2 动态规划题:状态定义决定你是否能在半小时内做对
中等难度的编程题里,动态规划几乎是必考。2017阿里这套题里有一道我印象比较深的三角形最短路径和,要求从三角形顶部出发,每次只能移动到下一层相邻的节点,求到底部的最小路径和。题目本身不新,但现场写的时候,很多人会卡在状态定义上。
我的建议是,拿到动规题之后,不要立刻写转移方程,先用一个具体的小例子在纸上推演。比如三角形只有三层,自顶向下走一遍,把每一步的选择标出来。这样你会很快发现,如果自顶向下递推,需要额外处理边界节点;而如果自底向上更新,只需要在原数组上操作,每一行都从下往上累加,最终答案就落在数组第一个位置。
更具体地说,定义dp[i][j]表示从底部走到第i行第j列这个节点的最小路径和。那么转移方程就是dp[i][j] = triangle[i][j] + min(dp[i+1][j], dp[i+1][j+1])。你不需要真的开一个二维数组,可以在原始三角形数组上原地更新,把空间复杂度降为O(1)。笔试环境里,空间和时间一样重要,能原地更新的地方尽量原地更新。
动规题的另一个常见问题是初始化。很多人会把dp数组初始化成全0,但有些题需要初始化为正无穷大,才能让“不可达状态”不会被错误地参与比较。边界条件不是写代码的最后一步,而是写代码前的第一步。建议以后拿到动规题,强制自己先写注释:状态含义、初始值、转移方向、答案位置。这四个注释写清楚了,代码基本不会乱。
3.3 图论题:先估算复杂度,再决定用哪种搜索
图论在这套笔试里不一定会以标准的“给你一张图求最短路径”出现,它可能包装成“有N个任务,某些任务依赖另一些任务,请给出一个可行的执行顺序”。这就是典型的拓扑排序。
如果你对拓扑排序很熟,那这道题是送分题,无非就是统计入度、把入度为0的节点放入队列、依次处理。但如果你不熟,现场硬推也能推出来,只是会慢很多。这里我想分享一个“两分钟估算法”:先看N的范围,如果N是10^3级别,那O(N^2)的朴素解法勉强能过;如果N是10^5级别,必须用O(N+E)的算法。
提到这,是因为很多人在笔试时明明写出了思路正确的代码,结果因为复杂度不达标,只过了一部分测试用例。图论题尤其容易出现这种情况,因为它本身就自带“遍历所有节点”的过程,稍不注意就会多一层循环。
另外,搜索题里还有个隐藏考点:递归深度。当N达到10^5时,DFS递归很容易爆栈,导致程序在接近答案前直接崩溃。如果你遇到一道需要遍历整棵树的题,第一时间应该考虑用显式栈代替递归,或者直接上BFS。我在当年笔试时,就吃过递归爆栈的亏,后来凡是看到“N最大可能超过10^5”的题,默认不写递归。
3.4 会写暴力解也是优势:从60分到100分的过渡路径
编程题不是只有AC和零分两种结果。很多在线笔试平台会按照通过测试用例的比例给分,意思是你哪怕只能通过部分样例,也能拿到一部分分数。所以我在笔试里一直坚持一个顺序:先写一个确定正确的暴力解法,把能拿的分数拿到手,再思考怎么优化。
举个例子,如果题目要求“在一个长度为N的数组里找两个数,使它们的和等于目标值”,你可以先写双重循环暴力解,这个版本在数据量小时一定能过。等他开始跑大数据用例的时候,你再用哈希表优化成O(N)。这样做的好处是,你永远不会因为某个优化没写完而落得一道题白卷。
还有一个小技巧是,编程题里如果题目明确说了“测试数据分为小规模和大规模”,你可以针对小规模写暴力逻辑,然后追加一段针对大规模数据的优化分支。这样的代码在工程里可能不太好看,但在笔试抢分场景下非常实用。
4. 在线笔试环境的隐性规则:判题方式直接影响得分策略
4.1 “本地能跑”和“平台通过”之间差了哪些细节
很多同学在本地IDE里测试代码通过之后,就直接复制到平台提交,结果发现报了编译错误或者答案错误。这中间差的往往不是算法,而是环境差异。
首先是编译器版本。某些旧版本编译器不支持新的C++11特性,如果你在本地用了高版本的语法,在平台上报错很正常。其次,平台对输入输出的要求非常严格,多一个空格、多一个换行都可能导致格式错误。更重要的是,部分平台会用多个测试文件连续测试你的程序,如果你的代码里包含system("pause")或类似阻塞语句,第一个用例通过后程序就停住不往下走了。
我在准备这套笔试的时候,专门把所有代码都改成“标准输入流读完再输出”的模式,并且不使用任何特定操作系统的命令。这个习惯一直保持到工作以后,对线上判题系统来说非常友好。
4.2 部分样例通过时,如何精准定位边界问题
提交之后返回“通过3/10个测试用例”是很常见的。这时候不要慌,更不要反复提交看运气。正确做法是回头检查三个地方:第一,数据范围,特别是数组索引是否可能越界;第二,是否考虑了空输入、单元素输入;第三,运算过程中是否可能发生整数溢出。
我遇到过很多次,代码逻辑完全正确,只是把某个变量的类型定义成int,而真实数据的乘积超过2^31-1,导致溢出后结果变成负数。这类问题你自己在本地造数据基本察觉不到,因为你会下意识地挑一些顺眼的输入。应对办法是,提交前把所有可能参与乘法的变量都声明成long long。一次到位,别省这个内存。
另一个思路是看题目给的样例。样例往往是经过设计的,它可能覆盖常规情况,也可能暗示边界。如果样例能过但你不知道错在哪,可以尝试构造几个极端情况:数组全相同、字符串全是空格、数字为最大值。这些数据不需要提交,你在本地跑一遍就能发现很多隐藏问题。
4.3 代码风格和提交时机:未必要“最后一个交卷”
在线笔试里,很多人习惯等到最后一分钟才交卷,觉得这样能多检查几遍。但笔试平台往往会限制提交次数,或者对每次提交都做全量测试,频繁提交反而心理压力越来越大。更稳妥的做法是:每道编程题写完后,先做一次本地全量测试,再一次性提交。
代码风格在笔试中看似不重要,其实影响你后续的查错速度。变量命名清晰、函数内部没有过长的嵌套,能让你在回头debug时快速定位问题。我记得有一次考试,编程题需要处理二维数组,我第一次写的时候把行和列的变量名定义成了a和b,写到后面完全忘了谁是谁,浪费了十分钟才理清楚。从那以后,我所有笔试代码都坚持用有意义的变量名,哪怕多敲几个字母。
还有一个值得注意的点:不要在附加题上投入过长时间。附加题是拉分项,不是必做项。如果你前面的基础题已经做完,可以尝试一下;如果编程题还没完全通过,优先保证编程题的完整提交。
5. 考场上真实的时间分配与心态管理经验
5.1 需要优先保证的“基础分”
把这套笔试题过一遍之后你会发现,分值分布大概是这样的:客观题占三到四成,编程题占四到五成,附加题占一到两成。如果你目标是拿到面试机会,不需要追求附加题满分,只要保证客观题的正确率和编程题前两题AC,就已经具备很大优势。
所以我建议做题顺序是:先快速扫一遍所有题目,标记出客观题里自己绝对有把握的题目,优先做掉;然后立刻进入编程题,先把第一题写了;回头再解决客观题里半会不会的题。不要从第一题按顺序一路做到底,因为后面编程题带来的心理压力会影响你判断前面的选项。
我当年就是这样操作的:先把所有单选做完,遇到多选犹豫的题先标记跳过,然后做第一道编程题,之后回来补多选,最后再处理后两道编程题。这个顺序让我在时间上始终留有缓冲,不会出现“编程题没时间写”的惨状。
5.2 遇到不会的题,先给自己的思考设置闹钟
笔试过程中最怕的不是遇到不会的题,而是卡在一道题上出不来。当时我做一道多选时,觉得每个选项都有道理,反复看了五分钟也没下定决心。结果等我一抬头,时间已经过去二十分钟,编程题还没来得及写。这个教训非常深刻。
后来我给自己定了一条铁律:任何一道客观题,思考时间超过两分钟就标记跳过。编程题则分开对待,如果读完题五分钟后还没有任何思路,就先放下,去做其他题,等到最后再来攻。当你换了个思路再回头看,往往能发现之前遗漏的突破口。
在线笔试系统一般都有题号列表,你可以通过标记功能把不确定的题跳过去,最后统一处理。不要觉得跳过就是认输,这是非常正常的应试策略。真正重要的是把整张试卷里那些“你本来就能拿分”的题全部拿到手。
5.3 笔试结束不等于终结:复盘与后续面试衔接
笔试结束后,哪怕你觉得发挥不理想,也建议花半小时把题目大致回忆一遍。一方面是为之后的面试做知识准备,因为面试官很可能会拿着你的笔试题目再追问一轮;另一方面,复盘能帮你发现自己知识体系的漏洞,比盲目刷十道新题更有效。
我当时在笔试结束后,第一时间把每道题的考点记在备忘录里,尤其是那些当场没做对的题。后来面试环节,面试官真的问了一道和笔试差不多的字符串处理题,因为我在笔试后专门整理过,所以回答得特别顺。这个经验让我意识到,笔试不只是“筛人工具”,它同时也是一个很好的自学清单。
另外,笔试之后如果收到了面试邀约,可以主动去了解自己笔试的失分点。很多公司的招聘系统会展示笔试报告,或者你可以在面试时表达“我笔试阶段在XX方面准备得不够,后来重新梳理了”。这会给面试官一种“这个同学在意每一次反馈”的印象,在某些情况下比对答如流更加分。
写在最后的个人经验
经历过多次大厂笔试之后,我的整体感受是:实习生笔试出题并没有想象中那么“高不可攀”,它更像是一场基础能力体检。题目设计者希望看到的是,一个人在有限时间内,能不能保持冷静、能不能分清主次、能不能把学过的知识稳定输出。这些都是工作中真正重要的能力。
如果你正在准备类似的大厂实习笔试,我的建议很直接:先摸清试卷结构,再分配复习时间;客观题讲究准确率,编程题讲究拿满基础分;考场上遇到不会的题果断跳过,最后再回攻。你不需要成为算法大神也能通过这类笔试,但你需要成为一个“在考试环境下依然可靠”的人。
希望这篇复盘能给你一些参考。祝笔试顺利。