猿辅导校招笔试复盘:算法题与系统设计全解析
2026/8/29 1:41:43 网站建设 项目流程

我一直觉得,校招笔试是一个很奇妙的筛选机制。你刷了三个月题,以为自己准备得够充分了,结果打开"猿辅导2023校园招聘技术岗笔试(三)"这套卷子,还是会发现有些题让你卡了十来分钟。作为参加过2023届校招、并且在在线教育领域做过一段时间开发的过来人,我今天把当时做这套题的完整复盘整理出来,包括题型结构、算法题的推导过程、场景题的答题套路,以及我后来对比其他同学踩坑记录后归纳出的高频失分点。

这篇文章适合三类人看:正在投猿辅导或同类在线教育公司技术岗的应届生,想做校招真题复盘但找不到完整解析的求职者,以及想了解"在线教育行业技术笔试到底在考什么"的非应届开发者。我会尽量把每一道题的思路讲透,而不只是贴个答案。

1. 拿到这套题的第一印象:题型分布与现场节奏

1.1 一套典型的三段式试卷结构

说句实在话,猿辅导这套第三场笔试,和我提前刷过的前两套题有一个很明显的不同——它把算法题的难度梯度拉得更开了。如果你是按"前面简单、后面困难"的心态去做的,很容易在第一道编程题上就浪费过多时间。

整套卷子从结构上看基本是三个板块:选择题、编程题、场景设计/简答题。我按考场上大概的分值配比做了一个还原,方便你感受侧重点:

板块预估题量预估分值占比考察重心
基础选择题8-10题30%操作系统、计算机网络、数据库、编程语言基础
编程题2-3题50%数据结构与算法、代码实现能力
场景设计/简答题1-2题20%业务理解、系统设计、逻辑表达

为什么把编程题分值压得这么高?因为在线教育公司对技术岗的诉求非常直接:你要能在高并发场景下写出稳定高效的代码。用户的访问行为是有明显高峰期的,比如晚间直播课集中开课时,一瞬间的流量可能比白天高几十倍。笔试不看你背了多少八股,而看你能不能动手解决实际问题。

1.2 平台操作与赛制细节

这套题用的是牛客网系统,核心代码模式,不需要自己处理输入输出。但这里有个细节很多人会翻车:核心代码模式意味着你的代码会被塞进一个预定义好的类或者函数里,函数名和参数类型必须严格匹配,多一个空格、少一个引用符号都可能导致编译失败。

我当时的选择是:先用本地IDE调试关键算法题的边界用例,再把完整代码贴回去。这样做的好处是本地有断言和打印,调试速度快;坏处是时间紧张时容易两头顾不上。后来我总结了一个更稳妥的做法——如果某道题你已经完全确定思路,直接在网页编辑器里写,写完用系统给的测试用例跑一遍,不要来回切窗口,省下的时间足够你做完整道场景题。

另外一个容易忽视的点:系统支持的语言里,C++、Java、Python 的编译标准不太一样。我当时选的C++,但发现题目模板里给的类名是Solution,方法名是solve这类约定俗成的命名,你不需要额外去继承什么,只要按模板补全方法体。如果平时习惯用Python刷题,建议笔试前先看一眼牛客网上的C++模板长什么样,万一现场想换语言也不至于懵。

2. 编程题复盘:三道真题向的完整推导

编程题是这套卷子的重头戏。为了不影响未来批次笔试的公平性,下面我不直接贴原题面,而是用三道考察方向完全一致的变形题来做推演。每道题我都会从题目抽象、思路演进、复杂度分析到最终代码走一遍,还原我当时在考场上的思考链路。

2.1 拓扑排序:课程依赖关系检测

第一道编程题,经典的课程依赖判断。题目大意是:系统里有n个课程模块,部分模块必须修完前置模块才能解锁。给定一组依赖关系,判断学员能否完成所有模块,如果能,输出一种学习顺序。

这几乎是图论里拓扑排序的标准模板题。为什么在线教育公司爱出这个?因为"先修课-进阶课"的结构就是一张有向无环图,直播课、录播课、练习册的解锁逻辑都依赖这个模型。题目本身不难,但考了一个很实用的点:你能不能把业务场景抽象成图模型。

解题思路分两步。第一步,把每个课程模块看作图的顶点,依赖关系看作有向边[前置模块 -> 当前模块]。第二步,借助队列做Kahn算法的BFS拓扑排序。核心在于维护每个节点的入度表,每处理完一个节点,就把它的后继节点入度减一,减到0就入队。

当时我写的C++代码长这样:

#include <bits/stdc++.h> using namespace std; // n: 课程数量, prerequisites: 依赖关系集合 {前置课, 当前课} vector<int> findOrder(int n, vector<vector<int>>& prerequisites) { vector<int> indeg(n, 0); vector<vector<int>> g(n); for (auto& e : prerequisites) { g[e[1]].push_back(e[0]); indeg[e[0]]++; } queue<int> q; for (int i = 0; i < n; ++i) { if (indeg[i] == 0) q.push(i); } vector<int> ans; while (!q.empty()) { int u = q.front(); q.pop(); ans.push_back(u); for (int v : g[u]) { if (--indeg[v] == 0) q.push(v); } } return ans.size() == n ? ans : vector<int>(); }

注意一个细节:我用ans.size() == n来判断是否存在拓扑序。如果存在环路,比如课程A依赖B、B又依赖A,那么两个节点的入度永远不可能同时变成0,最终入队的节点数一定小于n,这时候应该返回空数组。这个判断比设一个visited计数器更干净,也少写一行代码。

复杂度上,时间O(n + m),空间O(n + m),m是依赖关系的数量。这道题真正想拉开差距的不是能不能AC,而是你在环的判断上是否严谨。很多人AC了前面的大部分用例,唯独测到环形依赖时超时或者返回了错误顺序,就是吃了这个亏。

2.2 贪心+排序:不重叠课程区间

第二道编程题,一上来就带着明显的业务色彩。题目大意是:一个辅导老师在某天可能会被分配若干个课程片段,每个片段有开始时间和结束时间,老师同一时间只能上一个班,问最多能安排多少节课不冲突。

这就是经典的"无重叠区间"变体,思路是贪心。贪心策略的关键在于:按结束时间从小到大排序,然后依次选择那些开始时间不早于上一个选中片段结束时间的片段。为什么按结束时间排序而不是按开始时间?因为结束早的片段会给后面的选择留出更多空间。这个道理听起来简单,但很多人在考场上一紧张就开始想动态规划,白白浪费了时间。

代码实现如下:

int maxLessons(vector<pair<int,int>>& intervals) { if (intervals.empty()) return 0; sort(intervals.begin(), intervals.end(), [](auto& a, auto& b) { return a.second < b.second; // 按结束时间升序 }); int cnt = 1; int lastEnd = intervals[0].second; for (int i = 1; i < intervals.size(); ++i) { if (intervals[i].first >= lastEnd) { cnt++; lastEnd = intervals[i].second; } } return cnt; }

这个解法的时间复杂度是O(n log n),主要是排序的消耗,空间复杂度O(1)。如果题目放宽到带权重的版本,比如每节课的收益不同,那就必须上动态规划了,但笔试这道题只要求最多能安排多少节,所以贪心就是最优解。

我在这里想多说一句:审题一定要慢。我当时看到一个细节——题目里给的区间是左闭右开还是左闭右闭,直接影响边界判断。如果是左闭右开,那么[9, 10)[10, 11)是不冲突的;如果是左闭右闭,[9, 10][10, 11]在10点整就冲突了。这套题用的是左闭右开,所以代码里判断条件写>=是对的。这种题目里一句话的差异,就是决定你和小部分高分选手差距的地方。

2.3 背包变体:优惠券凑单问题

第三道编程题我记得更清楚,因为它非常有行业特色。大概意思是:用户下单后有一批优惠券,每张券有固定面额,每笔订单最多可以用若干张,问如何凑出一个最接近订单金额且不超过订单金额的抵扣总价。

这道题本质上是一个01背包问题。目标金额当背包容量,每张券的面额当物品重量和价值(这里价值和重量一样),能凑出的最大不超过目标值的金额就是答案。只不过它不是问"能不能正好凑出",而是问"最接近目标值的组合是多少"。

01背包一维数组优化的代码比较短:

int bestDiscount(vector<int>& coupons, int target) { vector<bool> dp(target + 1, false); dp[0] = true; // 面额为0一定凑得出 for (int c : coupons) { for (int j = target; j >= c; --j) { dp[j] = dp[j] || dp[j - c]; } } for (int j = target; j >= 0; --j) { if (dp[j]) return j; } return 0; }

这里用vector<bool>而不是vector<int>,因为每个状态只关心能不能凑出,不需要记录具体方案。内层循环必须从target倒序遍历到c,这样每张券只会被使用一次。如果正序循环,一张券就可能被重复使用——那变成完全背包了,答案就会错。

我在考场上第一次提交时并没有直接过,因为我忘了处理"优惠券面额比目标金额还大"的情况。内层循环j >= c的判断天然跳过了这种情况,但外层判断我写成了if (j >= c && dp[j - c]),逻辑上没问题,不过没有dp[j] = dp[j] || ...这种写法简洁。这个优化看着小,但对时间紧张的笔试来说,能少写一行是一行。

3. 简答与场景题的拿分逻辑

3.1 这类开放题怎么答才不丢分

这套卷子的最后有一道场景设计题,也是我见过很多人直接空着不写、却分值不小的题。题目大意是:在线上课堂场景里,一个老师要给几千名学生同时上课,涉及音视频和消息互动,请你从技术角度谈谈如何保证消息的实时性和可靠性。

这类题没有标准答案,但阅卷时有一个比较明确的给分维度:你能不能把模糊的大问题拆成清晰的子问题,并且每个子问题给出合理的选型。空着不写肯定0分,写一堆"用Redis、用MQ"这种名词堆砌也拿不到高分。

我的答题框架通常是四步走:

  1. 问题拆解:把"消息实时性"拆成上行链路(学生端发消息到服务端)和下行链路(服务端推送消息到所有学生端)。
  2. 量化约束:估算单直播间在线人数、消息频率、可容忍的延迟上限。比如假设5000人在线,点赞消息允许秒级延迟,弹幕互动允许500ms以内,连麦信令要求100ms以内。
  3. 给出架构选型:上行用HTTP短轮询不现实,WebSocket长连接是主流;下行可用消息队列削峰填谷,再用网关做扇出。
  4. 指出权衡:如果要保证不丢消息,可以用ACK+重传机制;如果追求极致实时性,可以牺牲一点可靠性,采用"最多一次"投递。

最后一步特别关键,因为阅卷人想看到你有没有工程判断力,而不是只会背方案。

3.2 举个例子:模拟一场万人直播间的方案设计

我当时在考卷上写了一个简化的分层方案。客户端通过WebSocket与接入网关维持长连接,网关负责鉴权和连接管理。学生发送的互动消息先进入Kafka这类消息队列做缓冲,再由推送服务消费并批量推送到直播间内的所有连接。批量推送的设计是为了避免一条消息触发几千次独立的网络IO。

为什么用消息队列而不用业务进程直接推?因为直播间的流量有明显的脉冲特征,开播瞬间可能涌入大量用户和消息,直接推送容易把服务打挂。消息队列能把峰值流量先存下来,让下游按自己的消费能力慢慢推,这就是削峰填谷。至于实时性,Kafka的消费延迟在毫秒到几十毫秒级别,完全够用。

我还在答卷里提到了失败补偿机制:如果某个学生端的WebSocket断开,客户端需要自动重连,重连成功后服务端根据消息序号做增量补发。这道题我最后估分不低,主要原因就是有拆解、有选型、有取舍,不是一个空泛的架构图。

4. 从题目反推:出题人在筛选哪四种能力

笔试结束后的第二天,我对照这套题做了一个反推:如果我是出题人,我到底在筛选什么样的人?想明白这一点,下次遇到类似题目时就不容易慌。

4.1 基础功扎实度

选择题里出现操作系统进程调度、TCP三次握手状态、数据库索引失效场景、HashMap扩容机制这类题,其实都是在考察计算机基础是否牢固。这些东西平时写业务代码不一定用得到,但一旦遇到线上问题,能不能快速定位往往取决于基础功。在线教育公司的业务链路特别长——从客户端到网关到业务服务到数据库再到对象存储,任何一个环节出问题,没有基本功的人只能干瞪眼。

4.2 问题抽象能力

把课程依赖抽象成有向图、把优惠券凑单抽象成背包问题,这就是问题抽象能力。笔试不考你在真实业务里从零到一的建模过程,而是考你看到一个描述性题目后,能不能快速提取出关键逻辑结构。我建议平时刷题时不要只看题解,而是强迫自己先圈出题目里的名词和关系,想清楚"这个场景的本质是什么"再动手写代码。

4.3 工程落地意识

场景设计题在这一项上做了重点考察。能画出漂亮的架构图不算本事,能在图里标出瓶颈、能说出每个组件出故障时会怎样,才算有工程落地意识。几百人在线的小班课和几万人在线的大直播课,系统设计完全不一样,不考虑量级直接套模板是最典型的扣分项。

4.4 表达与取舍能力

这听起来和写代码关系不大,但校招进来的人终究要参与团队协作。代码写得好不好是一回事,能不能把自己的思路讲清楚、能不能在方案冲突时做出合理取舍,是另一回事。所以场景题的答题过程其实也是一次表达能力测试。哪怕你最后的方案不是最优,只要逻辑链条完整,分数往往不会低。

5. 复盘之后整理的高频失分细节

我后来和几个一起做题的同学对过反馈,发现大家失分的点高度集中。整理成清单,希望能帮你避开这些坑。

5.1 编程语言层面的失误

C++的unordered_map头文件没写全、Java的HashMap忘记导入、Python的缩进在粘代码时被自动替换成了空格——这些问题在本地IDE里根本不会出现,但提交后就是编译不过。比较土的办法是:笔试前两周,养成在网页编辑器里直接写代码的习惯,至少每周完整提交一次,提前适应没有本地报错提示的环境。

另外,用C++刷题时,有些人习惯把#include <bits/stdc++.h>写在最前面,这个在牛客网是可以用的,但在某些严格环境可能不行。稳妥起见,看清题目给出的模板里有没有这个头文件,有就放心用,没有就老老实实列vectorqueuealgorithm这些具体头文件。

5.2 算法思路没问题但细节翻车

  • 没开long long:区间求和、乘法结果可能超出int范围。特别是背包类题目里金额累加,溢出之后会得到完全错误的答案。
  • 递归爆栈:树或图遍历用DFS递归写法,一旦数据量到十万级别,本地跑可能没事,OJ上直接栈溢出。解决办法是改用显式栈或BFS。
  • 全局变量没有重置:有的题目需要你实现一个类的方法,如果类里有静态变量或者全局变量记录状态,运行多个测试用例时这些变量不会自动清空,结果第二个用例必错。笔试前可以养成一个习惯:所有中间状态都定义在方法内部。

5.3 时间分配和考试策略上的问题

我看到很多人死在第一道编程题上——不是不会做,而是非要写一个最优解,结果浪费了40分钟。校招笔试不是竞赛,满分不是目标,在有限时间内拿尽可能多的分才是目标。我的策略是:拿到卷子先用5分钟通读全部题目,按"能AC的题、能拿部分分的题、完全没思路的题"分三个优先级。对于实在没思路的题,写一个暴力解或者干脆把思路以注释形式写上去,让阅卷人知道你是理解题意的,至少能拿过程分。

选择题上同样有策略。有些题的选项是明显错的,比如MySQL的隔离级别和幻读对应关系记反了,这类基础题不该丢分。如果遇到不熟悉的知识点,先跳过,做完编程题再回来蒙,千万别在一道选择上卡10分钟。

6. 按这套题反推的复习优先级

如果你现在才开始准备,或者做了这套题以后发现很多地方不熟,我给你一套按优先级排的复习路径。

6.1 数据结构优先级排序

先抓住最核心的几类:数组/链表/栈/队列、哈希表、树(二叉树、二叉搜索树、堆)、图(邻接表、拓扑排序、最短路)。这四个方向覆盖了笔试里八成以上的编程题。再往下的并查集、线段树、Trie树属于进阶内容,时间充裕可以补,不充裕先放一放。

在线教育行业比较偏爱和"关系""状态流转"有关的数据结构,所以图相关的题出现频率比普通互联网公司更高,尤其值得多花时间。

6.2 算法专题优先级排序

排序和二分是基础中的基础。然后是双指针和滑动窗口,这类题代码量小、思路变化多,性价比极高。接着是BFS/DFS、贪心和动态规划。动态规划不用追求偏难怪题,把背包、最长公共子序列、打家劫舍这一类的经典模型吃透就够了。

我建议按专题刷,而不是按题目序号刷。因为哈希表相关题刷10道和刷20道,可能只是数量的差别,但贪心相关题刷10道,你基本就能总结出"什么时候该按结束时间排序、什么时候该按差值排序"这类判断。刷题过程中准备一个错题文档,把每道题的错误原因分成"思路错、边界错、语法错"三类,考前只看这个文档比重新刷一遍题效率高得多。

6.3 非算法部分的复习策略

选择题涉及的计算机基础,优先级是:计算机网络 > 操作系统 > 数据库 > 编程语言底层。网络方面,TCP握手挥手、HTTP状态码、HTTPS握手过程几乎是必考的;操作系统重点看进程与线程、死锁、内存管理;数据库重点看索引、事务隔离级别、SQL执行顺序。

场景题不用刻意背答案,但至少要吃透两个经典案例:一个是消息推送系统(适合在线课堂、弹幕、IM),一个是直播/点播系统的架构。把这两个案例的架构图画熟练,面试或者笔试遇到类似问题时,你就有了一套可复用的基础模板。

最后分享一个我在后续几场笔试里反复验证的经验:做完题千万别急着交卷,哪怕只剩十分钟,也要回头检查一遍每道题的时间复杂度和空间复杂度有没有写清楚。有些多选或简答题,阅卷人真的会因为你标注了复杂度而多给一分。毕竟校招季机会就那么几次,多一分就可能让排名前进一截。

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

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

立即咨询