笔试题这东西,最怕的不是难,而是“看着眼熟、动手失分”。网易2018校招研发工程师(有道事业部)笔试卷,在当年刷题社区里被讨论过很多轮,我后来也拿它给带过的同学做过模拟,它属于典型的“大厂通用基础题+两道半经典算法题”的组合:选择题覆盖面广、单题不深,编程题则特别考验边界思维和算法反推能力。这篇文章不是原始试卷的逐字誊抄,题目按回忆还原,而是把它当作一份样本来拆解:考了哪些点、为什么这样考、编程题的解题路径是什么,以及如果你现在准备互联网校招笔试,这套题能给你什么参考。如果你正在准备大厂研发岗,尤其是网易、有道这类做C端产品技术团队的校招,这篇文章值得读完并对照刷题。
1. 试卷全貌:题量、时间线与考点分布
1.1 笔试总体构成
网易2018校招研发工程师(有道事业部)笔试卷,整体是在牛客网这类在线笔试平台完成的,形式上分为两大部分:一部分是选择题/多选题,覆盖计算机网络、操作系统、数据库、数据结构与语言基础;另一部分是2到3道在线编程题。从我当年做题以及后来复盘时看到的版本来看,编程题一般是3道,分数权重相当大,通常占了总分的一半以上,直接决定能否进入面试环节。
时间上,整套卷一般是90分钟到120分钟。这里有个很容易被忽略的点:选择题部分不要恋战。因为在线编程题的读题、调试、提交都要时间,如果前面选择每道题都磨两三分钟,后面大概率会写不完。我见过不止一个基础不错的人,前面选择做得太细,最后一道编程题只写了半截,非常可惜。
1.2 有道事业部的技术栈倾向
网易有道当时的主力产品是有道词典、有道翻译、有道云笔记等,业务上强依赖搜索、推荐、自然语言处理和在线服务的高可用架构。反映到笔试试卷上,你会发现它不像某些游戏公司那样死磕图形学或引擎,也不像电商公司那样狂考分布式缓存和消息队列,而是更偏向通用工程能力:网络协议、操作系统、内存管理、数据结构,都是研发工程师的基本功。这一点对备考方向是很有价值的信号。
另外,语言没有限定死。C++、Java都可以,但试卷里关于C++虚函数、内存布局的题出现的频率不低。如果你用Java,选择题遇到C++语法也不要慌,用内存模型和多态的那套通用知识去理解,基本能推出来。编程题则建议用你最熟练的语言写,在线判题系统对常见语言都支持,没有必须用某一种语言的限制。
从整体试卷组织来看,可以用一张表概括:
| 题型 | 大致数量 | 建议用时 | 考察重点 |
|---|---|---|---|
| 单选/多选 | 15~20题 | 30分钟 | 网络、操作系统、数据库、语言基础 |
| 编程题 | 3题 | 50~70分钟 | 字符串、搜索/最短路、逆向推导、边界控制 |
| 简答/设计(部分批次) | 1题 | 剩余时间 | 系统设计或业务场景分析 |
这套结构说明,试卷想筛选的不是“背了多少八股”,而是“基础是否扎实、代码能否在限定条件下跑对”。
2. 选择题复盘:计算机基础里的“送分题”与“陷阱题”
2.1 网络与并发的经典组合
选择题中,计算机网络是必考模块。我印象里出现过一道关于TCP三次握手的题,选项集中在“两次握手行不行”“第三次握手丢失会怎样”。这道题表面问握手,实际考的是对“连接建立为什么需要确认”的理解。答案是:两次握手无法防止已经失效的连接请求报文突然又传到服务器,导致服务器建立空连接、浪费资源;如果第三次握手的ACK丢失,服务器会重传SYN+ACK,客户端根据情况处理。这种题没有技巧,理解状态迁移图之后基本不会错。
操作系统部分,比较经典的考法是“进程和线程的区别”以及“死锁”。死锁那道题,把四个必要条件——互斥、持有并等待、不可剥夺、循环等待——各包装成一句话,让你选出“不是必要条件”的选项。平时背概念的人容易选错,因为四个条件都像是对的;真正理解的人应该知道,现代操作系统在资源分配时引入“资源剥夺”策略,正是为了打破其中某个条件。送分和陷阱往往就在这种地方。
2.2 语言和数据库里的边界细节
语言基础这块,C++的虚函数表、静态绑定与动态绑定是高频题。靠死记硬背“基类指针调用虚函数走动态绑定”还不够,题目往往套了一层继承:基类析构函数不是虚函数,通过基类指针delete派生类对象会发生什么。答案大家都知道是未定义行为,但很多同学说不清原因。原因是析构函数不虚,delete时按静态类型调用析构函数,派生类部分没有被析构,内存泄漏只是表象之一。这段我在给同学讲的时候常开玩笑:笔试不考你写了多少代码,考你有没有踩过底层内存的坑。
数据库索引也是经常考的。网易的题目爱把B+树索引和Hash索引放在一起比较,问哪种支持范围查询。B+树因为叶子节点有序且用链表串联,天然支持范围扫描;Hash索引精确匹配快,但不适合范围查询。选错的同学一般是没注意“范围查询”这个关键词,光看到“索引快”就选了Hash。其实命题人很喜欢在这种“看似都行、实际有明确场景”的地方挖坑。
还有一类是“缓存淘汰策略”,比如LRU和LFU的区别。题目会给出一个访问序列,问你按LRU淘汰会留下哪些页面。这种题本身不难,但考场上容易因为“最近最少使用”和“最不经常使用”两个概念混淆而丢分。我的经验是,遇到这类题先在草稿纸上把访问序列和当前缓存写下来,一步一画,避免脑内模拟出错。
3. 三道编程题解析:从读题到AC的完整思路
编程题部分我想写得细一点。三道题代表了三类很典型的校招算法题:模拟题、搜索/最短路题、逆向推导题。它们都不是竞赛难度的题,但都能有效筛掉“只会背模板、不会分析边界”的候选人。
3.1 字符串压缩:一上来就考边界条件
题目大意(回忆还原版):给定一个字符串,例如 aaabbbc,要求将连续出现的相同字符压缩为“次数+字符”的形式,即 3a3b1c;如果压缩后的字符串长度不小于原串长度,则输出原串。请实现这个压缩函数。
这个题第一眼以为是“遍历+计数”的送分题,真正拉开差距的是几个边界:
- 空字符串不能数组越界;
- 连续字符在结尾时,最后一次计数要正常写入结果;
- 压缩后长度比较要用“新生成的字符串长度”和原串比较,而不是计数时比较。
我见过有人写完主逻辑,结果输入空串直接崩了;也有人忘记“压缩后更长则输出原串”,导致 aa 被输出成 2a,虽然题目要求下应该还是 aa。为什么强调这个条件?因为这道题在实际工程里对应的场景是传输数据压缩,如果压缩后反而更大,那保留原始数据才是合理策略。把工程语义装进考题,是网易这类公司出题时的典型手法。
C++参考实现:
#include <iostream> #include <string> using namespace std; string compress(const string& s) { if (s.empty()) return ""; string res; int count = 1; for (size_t i = 1; i <= s.size(); ++i) { if (i < s.size() && s[i] == s[i - 1]) { ++count; } else { res += to_string(count); res += s[i - 1]; count = 1; } } return res.size() < s.size() ? res : s; } int main() { string s = "aaabbbc"; cout << compress(s) << endl; // 3a3b1c return 0; }复杂度的关键:to_string(count) 不会影响整体线性复杂度,O(n) 时间、O(n) 空间。易错点集中在遍历条件上:我把 i 走到 size() 位置,用 i < s.size() 判断是否还能取字符,保证最后一个连续段也能被收尾。这种写法比在循环里单独再写一段“处理尾部”的逻辑更不容易漏,我个人测试下来更稳。
3.2 跳石板:披着“状态枚举”外衣的最短路问题
题目大意(回忆还原版):小易站在编号为 N 的石板上,目标跳到编号为 M 的石板。他每次只能往前跳“当前石板编号 K 的一个非 1、非自身的约数”这么多步,也就是从 K 跳到 K + X,其中 X 是 K 的真因子,K 的约数中同时要满足小于 K 且大于 1。问最少跳几次,如果无法到达输出 -1。N、M 在十万数量级内。
这道题当年在网上被打上“网易经典题”标签,核心问题在于:它既可以用 BFS 解,也可以用 DP 解,但两种解法的正确性都比较隐蔽。
先说 BFS。把石板编号看作图的顶点,编号 K 能一步到达 K + d,其中 d 为 K 的真因子,这就是一个有向无权图,最少跳数就是最短路,BFS 天然适合。关键是怎么高效枚举 K 的所有真因子:循环 i 从 2 到 sqrt(K),如果 i 整除 K,那么 i 和 K / i 都是因子,注意排除 i == K / i 这种重复,同时两个因子都要大于 1 且小于 K。实现细节上,K / i 可能等于 K 吗?不会,因为 i 至少是 2。K / i 可能等于 1 吗?当 i 等于 K 时才会,但我们只在 i * i <= K 的范围内枚举,所以也不会。这样跳石板这层逻辑就是干净的。
C++参考实现(BFS):
#include <iostream> #include <vector> #include <queue> using namespace std; int jump(int N, int M) { if (N > M) return -1; vector<int> dist(M + 1, -1); queue<int> q; dist[N] = 0; q.push(N); while (!q.empty()) { int cur = q.front(); q.pop(); if (cur == M) return dist[cur]; if (cur > M) continue; vector<int> factors; for (int i = 2; i * i <= cur; ++i) { if (cur % i == 0) { factors.push_back(i); if (i != cur / i) factors.push_back(cur / i); } } for (int f : factors) { int nxt = cur + f; if (nxt <= M && dist[nxt] == -1) { dist[nxt] = dist[cur] + 1; q.push(nxt); } } } return -1; } int main() { int N = 4, M = 24; cout << jump(N, M) << endl; return 0; }容易踩的坑有三个。第一,不把“是否已经访问过”的数组加进来,直接放进集合里判断,可能重复入队很多次,造成超时;第二,当前石板编号 cur 如果是质数,它没有真因子,那这层自然无法扩展,不能漏掉这种情况的判空;第三,如果 cur 已经大于 M,直接跳过,因为题目只能往前跳,不可能跳回小编号。
当然,这题也可以用动态规划:从 N 出发,对每个能到达的位置更新后续位置,dp[nxt] = min(dp[nxt], dp[cur] + 1)。两种写法复杂度接近。我在面试辅导时推荐首选 BFS,因为最短路的语义更直观,逻辑上不容易出现“动规顺序没想清楚”的问题。DP版本最常见的错误是只扫描一遍数组,但并没有按最短路步数顺序去更新,导致某些位置被更新了多次却没有收敛。你如果要用DP,记得是不断迭代直到没有更新发生,或者按BFS顺序处理。
3.3 魔法币:逆向推导一行公式
题目大意(回忆还原版):小易有两台魔法机器。往机器1投 x 个魔法币,机器会吐 2x+1 个;往机器2投 x 个魔法币,会吐 2x+2 个。小易开始有 0 个魔法币,需要恰好 n 个魔法币,请输出机器的投入顺序,用字符串表示,机器1对应字符'1',机器2对应字符'2'。
这题的精髓是反向思考。如果最后一台机器是机器1,那么投入前的硬币数 x 满足 n = 2x + 1,所以 n 一定是奇数;如果最后一台是机器2,那么 n = 2x + 2,所以 n 一定是偶数。于是我们从 n 开始,每次根据奇偶性反推出上一步,一直反推到 0,最后把记录下来的机器序号反转,就是顺序。
注意边界:n = 0 时其实不需要输出,但题目里 n 至少是 1,因为需要恰好 n 个来购买神器。n = 1 时,1 是奇数,反推 x = 0,输出 '1',也就是单独投一次机器1,得到 1 个魔法币,正确。
C++参考实现:
#include <iostream> #include <string> #include <algorithm> using namespace std; string magicCoins(int n) { string ans; while (n > 0) { if (n % 2 == 1) { n = (n - 1) / 2; ans.push_back('1'); } else { n = (n - 2) / 2; ans.push_back('2'); } } reverse(ans.begin(), ans.end()); return ans; } int main() { cout << magicCoins(10) << endl; return 0; }这道题对“正向构造”习惯强的同学反而是个心理考验。很多人在考场上拿到题第一反应是 BFS 去找最短序列,但仔细看题会发现,它要的是任意一个合法顺序,不是最短顺序。一旦意识到机器输出与输入存在严格奇偶对应关系,整个问题就从搜索变成了简单数学推导。这类逆向思维题在校招笔试里频繁出现,本质上考察的是建模能力,而不是堆数据结构。
4. 复盘后总结的命题规律与做题节奏
4.1 网易系笔试的三条命题惯性
第一,题目场景化,核心算法简单。跳石板包装成“走石板路”,魔法币包装成“魔法王国购买神器”,但背后就是 BFS 和奇偶推导。不要被场景吓到,读题时把名词剥掉,留下数据结构和数学关系。
第二,边界条件比算法本身更拉分。字符串压缩里“压缩后更长则输出原串”就是典型。很多人把主逻辑写对,却在边界上挂了测试用例。在线判题有隐藏用例,编译通过不代表AC,笔试系统的判题通常包含很多边界用例,比如空串、单字符、最大值、完全不能到达等。我复盘时经常看到,一道题大家主思路都差不多,最后区分度全在那些只有一两行的边界处理上。
第三,暴力解法常常会被打回。跳石板如果你用 DFS 一次性枚举所有路径,状态空间会非常大,提交后超时几乎是必然的。考场上写代码,先想复杂度再动手,是一个专业工程师的习惯。这也是笔试筛选的目的——算法思路是否具备复杂度意识。
4.2 90分钟怎么分配才算合理
结合这套卷,我的建议是这样。如果总分值100分,选择题占40分左右,编程题占60分左右。那时间分配大概就是:选择题最多30分钟,留10分钟机动,60分钟给编程题。三题中先做自己最有思路的,不要被题序绑架。比如有人字符串处理强,就先做第一题;有人对搜索类敏感,就先做跳石板。先拿稳一道 AC,比三题都只写了一半要强太多。
做题时还要留出“重读一遍输出格式”的时间。在线判题对输出格式非常严格,魔法币这道题如果多输出了空格,可能整题 WA。考场上每道题提交前,把题目里的输出样例复制成测试用例自己跑一遍,这个小动作能救回不少分。还有一个细节:如果本地跑样例通过但提交后WA,优先检查是不是把输入输出理解反了,尤其是题目给了“多组输入”还是“单组输入”这种说明,最容易看漏。
5. 以这套题为参照,校招笔试该怎么准备
5.1 按考点分块刷题,别靠题海硬怼
很多同学准备校招时喜欢刷题量,一天十道、三十道地刷,但实际上笔试的考点是有限的。计算机网络、操作系统、数据库、C++/Java语法、数据结构、常用算法,每一类都是独立的小模块。我建议按“两周一个模块”的节奏来。第一周收集该模块的常考概念和选择题,第二周集中做该模块对应的算法题。比如复习到图的最短路时,把 BFS、Dijkstra、Floyd 全放在同一周内对比练习,这样考场上遇到跳石板这样的题,你能立刻把问题归类。
刷题工具上,LeetCode 的热门题单和牛客网的真题区都值得做。牛客的真题区有个好处是保留了真实公司的选择题,很多都是历年考过或改过的。多做几套之后,你会发现命题人喜欢的考点确实是重复的:虚函数、B+树、死锁、三次握手、哈希冲突、LRU,翻来覆去就是这些。
5.2 笔试当天的心态与时间管理细节
笔试当天稳定心态最重要。我见过很多同学线上笔试一紧张,选择题看不到一半就开始慌,编程题第一题卡住之后就崩了。这里有两个实操建议:第一,开考后先花两分钟快速通读整卷,看看编程题大概是什么类型,在草稿纸上记下每道题的第一直觉;第二,遇到卡壳超过10分钟的题先跳过,做完其他部分再回来。跳石板这种题如果一开始没想通,先去做魔法币,返回来往往就有思路了。
最后再分享一个我复盘校招真题时保持的习惯——每道编程题 AC 之后,不急着提交下一题,而是再花几分钟想:如果题目的限制条件变化,比如 M 从十万变成一亿,我的方案还能过吗?这种思考能帮你把一道题变成一类题,校招笔试拼的才不是谁刷得多,而是谁能在有限时间把见过的题真正吃透。