2016年我在图书馆刷完网易研发工程师笔试题(二)那个晚上,印象最深的反而不是哪道题不会做,而是部分题目“明明知识点都见过,考场上一紧张就判断错了”。现在回头看,这套题的价值在于它把研发岗核心能力拆成了几个固定维度:编码基本功、操作系统与网络、语言底层、概率建模。哪怕今天再面试,这些底子依然适用。
这篇复盘我不打算按套题顺序逐题讲,而是挑几类有代表性的题目,把当时的解题思路、容易踩的坑、以及背后真正想考察的能力拆开说。如果你正在准备校招笔试,或者想系统自查计算机基础,这篇内容应该能帮你省不少时间。
1. 算法编码题:考场上的时间分配与边界处理
网易这类互联网公司的笔试编码题,通常不会给一道纯模板题,而是喜欢在“常见解法”基础上换一个条件,考察你能否快速判断出正确的算法模型。2016这套题里有两道题我印象很深,一道是数组相关,一道是二叉树相关,都属于“看似简单,实际上手才发现问题不少”的类型。
1.1 有序数组合并求第K大:从暴力解到二分排除
题目大意是给定两个升序数组 A、B 以及一个整数 K,要求找出两个数组合并后的第 K 大的元素。很多人的第一反应是“归并排序的合并过程”,直接把两个数组合并到一个临时数组,然后按下标取第 K-1 个。这种解法的时间复杂度是 O(m+n),空间复杂度也是 O(m+n),如果题目没有限制,这么写确实能过。
但笔试的编码题往往有隐含要求。网易这道题给出的函数签名是int findKth(int A[], int m, int B[], int n, int K),并没有给出时间和空间限制。可一旦看穿出题人的意图,就知道他在等你用二分排除法。
二分排除的思路是这样的:要找两个有序数组的合并第 K 大,每次比较 A 数组的第 K/2 个元素和 B 数组的第 K/2 个元素(注意下标从 1 开始数),假设aMid = A[K/2 - 1],bMid = B[K/2 - 1]。如果aMid <= bMid,说明 A 的前 K/2 个元素中,最多只有 K/2 - 1 个比 bMid 小,而 B 中至少有 K/2 个元素大于等于 bMid,所以 A 的前 K/2 个元素无论如何都不可能成为合并后的第 K 大,可以一次性丢掉。然后问题就缩小成在剩下的 A[K/2:] 和完整的 B 中找第 K - K/2 大。每次丢弃一半,时间复杂度降到 O(log(m+n))。
这里有两个边界条件最容易写错。第一个是当 K/2 大于某个数组长度时,就只能取那个数组的最后一个元素,或者直接处理另一个数组。第二个是当 K=1 时,直接返回min(A[0], B[0])。还有一个小陷阱:如果某个数组已经空了,问题就退化成在单个有序数组里找第 K 个元素,直接返回A[K-1]即可。
注意:笔试里这类“递归 + 边界条件”的题型,除了正确性,阅卷还会看你对边界条件的处理。哪怕你用的是 O(m+n) 的解法,只要边界判断完整,得分也不会低;但如果你用二分却让某个数组越界,扣分会很严重。
我当年写的就是先归并再取下标,代码量虽然多了,但胜在稳。后来复盘时才发现,二分排除才是出题人真正想考察的点——因为这道题在 2016 年前后经常作为一面算法题出现,放到笔试里其实是在筛掉“只会背模板”的人。
1.2 二叉树的完全性判断:层序遍历的边界意识
另一道编码题是判断一棵二叉树是否是完全二叉树。什么叫完全二叉树?除了最后一层之外每一层都被填满,且最后一层的节点都靠左排列。这听起来很好理解,但很多人写代码时只知道“层序遍历”,一旦遇到“左子树为空但右子树不为空”的节点,就容易漏判。
我当时采用的方案是:借助队列做层序遍历,在遍历过程中维护一个mustBeLeaf标志。当第一次遇到某个节点没有左孩子或没有右孩子时,就把标志置为 true;之后再遍历到的任何节点,如果它还有左孩子或右孩子,就返回 false。
还有一种更简洁的写法:按层序遍历把所有节点(包括空节点)依次入队,当从队列中弹出第一个空节点后,如果队列中还有非空节点,说明不是完全二叉树。这两种方法等价,但第二种写法更考验对“空节点”的处理,代码也更短。
我在考场上用的是标志位方法,写完后又自己找了几个用例验证。比如一棵只有右孩子没有左孩子的树,第一层根节点入队,左孩子为空、右孩子非空,按完全二叉树的定义直接判负;又比如最后一层节点不靠左的树,遍历顺序会很自然地暴露问题。
这类题之所以高频,是因为它把“对数据结构的理解程度”和“对边界条件的敏感度”绑在了一起。很多候选人能写出层次遍历,却忘了判断“节点可能缺失孩子”带来的连锁影响。
1.3 时间分配策略:适可而止
网易这套题中算法题大概有三四道,除了上面两类,还有一道字符串处理题,难度都不算变态。我的经验是:笔试卷子上每道题的分数权重不一样,如果某道题卡了 15 分钟以上还没思路,果断先跳,把后面会做的题写完再回来。编码手写题最忌讳“死磕一道题导致卷面大片空白”,因为阅卷是按点给分,有部分正确思路也会给分。
2. 操作系统与计算机网络:不是死记硬背,是场景判断
研发工程师笔试中,操作系统和计算机网络基本是必考模块。网易这套题的特点是:不直接问你“什么是死锁”,而是给出一段场景描述,让你判断哪几个选项是死锁发生的必要条件。这种问法更贴近真实工作里的问题定位,因为你在排查线上进程卡死时,面对的就是一堆表象,而不是一道定义题。
2.1 进程与线程:为什么“线程拥有独立栈”不是错误选项
有一道经典选择题大概是这样:关于进程和线程,下列说法正确的是?
备选项里通常会出现“线程拥有独立的地址空间”“线程拥有独立的栈”“进程间可以通过共享内存通信”“线程切换开销比进程切换大”。这道题的迷惑性在于,很多人背过“线程共享进程的地址空间”,所以看到“线程拥有独立的栈”就以为是错的。这里一定要区分两个概念:线程共享的是进程的地址空间和堆资源,但每个线程都有自己的栈和寄存器上下文,否则函数调用时的局部变量就无法隔离。
所以正确的说法是:线程有独立的栈,但没有独立的地址空间;进程间通信方式包括共享内存、管道、消息队列等,其中共享内存效率最高;线程切换通常比进程切换开销小,因为线程共享地址空间,不需要切换页表。如果选项里有“线程拥有独立的栈”,它是正确描述,不是陷阱;如果选项里有“线程拥有独立的地址空间”,那才是错误说法。
这道题考完很多同学都对答案有争议,本质上是把“共享”理解得太绝对。你可以这样记:线程之间“共享代码段、数据段、堆”,但是“栈和寄存器上下文”是各自独立的。这个细节在后续并行编程里也至关重要,尤其是排查栈溢出时,要知道每个线程栈的大小是独立分配且有限制的。
2.2 死锁的四个必要条件与银行家算法
另一个死锁相关题目给了一个资源分配场景,要求判断当前系统是否处于安全状态。这其实就是银行家算法的简化版,数据规模不大,可以用表格手算。死锁发生的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。预防死锁的思路就是破坏其中一个条件。
我在考场上的做法是:先把每个进程还需要多少资源算出来,然后模拟分配顺序,看是否存在一条“所有进程都能完成”的序列。这个过程不难,但很考验细心程度。网易这道题的数据稍微设计了一下,初始可用资源给得刚好够某一个进程完成,如果你选错了第一个执行的进程,后续分配就会卡住。
这里有个笔试技巧:看到银行家算法的题,优先找“当前可用资源能满足哪个进程的剩余需求”,然后顺着这个顺序去推。如果有多个进程都能满足,随便选一条路径,只要证明存在安全序列即可。如果算不出来,再检查自己是不是把“已分配资源”和“剩余还需资源”搞混了。
2.3 TCP 三次握手与拥塞控制:经典但未必全对
网络部分考了 TCP 三次握手和拥塞控制的基础概念。三次握手本身不难,但题目可能会在选项里混入“第二次握手同时携带 ACK 和 SYN 标志”“第三次握手失败时服务端会发送 RST 报文”这类有争议的细节。
我记得那道题考的是拥塞控制机制:慢启动阶段拥塞窗口是指数增长的,达到慢启动阈值后进入拥塞避免阶段改为线性增长,一旦超时阈值减半,如果收到三个重复 ACK 则执行快重传、进入快恢复。这几个阶段在《计算机网络》教材里都有,但把人放在笔试环境里,很容易把“慢启动阈值”和“拥塞窗口”搞混。
我的建议是遇到这类题,直接画一个时间轴分段:1 到 4 轮是慢启动指数增长,4 到 8 轮是线性增长,第 9 轮超时后阈值减半。一来缩短计算时间,二来不容易错。网络部分很多题目其实靠画图就能解决,比纯记忆可靠得多。
3. C/C++ 与 Java 语言底层:虚构题与送命题
网易 2016 年研发工程师笔试题里,语言基础部分占比不低。这套题的特色是“知道就是知道,不知道就是不知道”,几乎没有蒙的余地。尤其是 C++ 的虚函数机制和 Java 的容器类,几乎年年出,年年有人错。
3.1 构造函数为什么不能是虚函数
有一道 C++ 题目:构造函数能否声明为虚函数?为什么?
答案是不能。原因要从虚函数机制本身说起:虚函数调用依赖对象的虚函数表指针(vptr),而 vptr 的初始化发生在构造函数体内。如果构造函数本身是虚函数,调用它时虚表还没有初始化,系统就无法确定该调用哪个版本的构造函数,形成“先有鸡还是先有蛋”的死锁。析构函数则相反,通常建议把基类析构函数声明为虚函数,否则通过基类指针删除派生类对象时,只会调用基类析构函数,导致派生类资源泄漏。
这道题顺带还会考虚函数表的内存布局:单继承下一个对象开头有一个指向虚函数表的指针,多重继承下对象可能有多个 vptr。如果有“在构造函数中调用虚函数会怎样”的变体题,你要知道:构造函数中调用虚函数不会发生多态,因为此时派生类部分还没构造完成,虚表指针指向的仍然是当前类的虚表。这个考察点在真实 C++ 开发里很有用,尤其是做插件系统和框架设计时。
3.2 Java HashMap 与 Hashtable 的对比陷阱
Java 部分有一道题:关于 HashMap 和 Hashtable,下列哪个说法是错误的?选项里有“HashMap 允许 null 作为 key”“Hashtable 是线程安全的”“HashMap 的默认容量是16”“Hashtable 允许 null 作为 value”。
答案是“Hashtable 允许 null 作为 value”是错误的。Hashtable 既不允许 null key 也不允许 null value;HashMap 则两者都允许。原因是 Hashtable 是 JDK 1.1 就存在的遗留类,它通过给整个方法加 synchronized 保证线程安全,而 HashMap 设计上就不是线程安全的,所以没有这个限制。如果你用 HashMap 做缓存且没有做并发控制,在多线程环境下很可能会出现数据错乱,这在 2016 年那道题里没直接问,但在面试环节经常被追问。
还有一道与 HashMap 相关的扩展题:HashMap 的扩容机制是怎样的?如果了解 JDK7 的链表头插法,就会知道并发扩容时可能形成循环链表,导致下次查询发生死循环。这个知识点虽然 2016 年笔试题里没有直接出现,但作为研发工程师,尤其是做服务端的同学,今天依然值得深入了解。
3.3 语言题的学习方法:从“背结论”到“构造场景”
我体会最深的一点是:语言底层题不能只背结论,必须能解释“为什么”。如果你只知道“构造函数不能是虚函数”却说不清 vptr 的初始化顺序,面试官会认为你只是背了八股。反过来,如果你能画出一个单继承、多重继承的内存布局图,即使当时答案写错了,面试官也愿意给你机会。
4. 概率智力题:考察的是建模,而不是结果
网易笔试比较喜欢在试卷末尾安排一两道概率题或智力题,这类题目单看答案可能不难,但它考察的是你把现实问题转化成数学模型的能力。2016 年这套题里有一道红球白球取球问题,一道抛硬币求期望问题,都是那种“看起来像公务员行测,实际上考的是状态机”的题型。
4.1 红球白球取球问题:奇偶性是不变量
题目大概是:袋子里有若干个红球和白球。每次取出两个球,如果两个球同色,就放回一个红球;如果两个球异色,就放回一个白球。问最后剩下来的球是什么颜色?
我第一次做的时候试图去枚举每一种可能,结果发现状态空间很大,很快放弃了。后来看标准答案解析才发现,这道题的核心是不变量:白球数量的奇偶性始终不变。
为什么?每取一次球,袋中球的总数减一。同色(红红或白白)时,取出两个球放回一个红球,白球数量要么减 2(白白)要么不变(红红);异色时,取出红白各一个放回一个白球,白球数量不变。因此白球数量的变化只有“减 2”和“不变”两种可能,奇偶性永远不改变。最后袋中只剩一个球时,如果初始白球数量是奇数,那最后一定是白球;如果初始白球数量是偶数,那最后一定是红球。
这个“找不变量”的思维比这道题本身重要得多。我在后来的算法工作中经常遇到类似场景,比如判断一个状态的变换是否可逆、在多线程并发中找哪些变量是守恒的,本质上都是同一套思路。笔试里出现这种题,并不是想难为你,而是想看看你有没有“从变化中找不变”的直觉。
4.2 抛硬币的期望次数:状态转移与方程思想
另一道概率题是:抛一枚均匀硬币,直到连续出现两次正面朝上就停止,求抛硬币次数的期望。这道题的常见解法是利用“状态”设方程。设 E0 表示当前已经连续 0 次正面时,还需要抛多少次才能结束;E1 表示当前已经连续 1 次正面时,还需要抛多少次才能结束。
从 E0 的状态抛一次,如果反面(概率 1/2),回到 E0 的状态;如果正面(概率 1/2),进入 E1 的状态。所以有E0 = 1 + (1/2) * E0 + (1/2) * E1。从 E1 的状态抛一次,如果反面,回到 E0;如果正面,游戏结束,所以E1 = 1 + (1/2) * E0。联立这两个方程,解得 E0 = 6。
这道题如果直接硬算“第一次正、第二次正”的期望,很容易算错。而用状态转移方程,思路清晰得多。这种思想在后面学习马尔可夫链和排队论时也经常出现,尤其做系统性能建模时,你会反复用到“当前状态 + 转移概率 + 期望方程”的框架。
4.3 考场策略:先列状态,再动笔
遇到概率题,我现在的建议是:先确定问题能用几种状态描述,再写出状态之间的转移关系,最后解方程。即使最后解不出来,把方程写到卷面上也能拿到部分分数。千万不要直接套某个公式,概率题的坑在于题目条件一变,公式就失效。
5. 这套题给后来者的三点启发
最后聊一点个人的复盘体会,不一定每条都适用于每个人,但应该能帮刚准备笔试的同学少走弯路。
第一,刷题之前先给自己列一份“知识点清单”。网易这套笔试题覆盖的范围很固定:数据结构(数组、链表、二叉树)、算法(排序、二分、动态规划)、操作系统(进程线程、死锁、内存管理)、网络(TCP/IP、HTTP)、语言基础(C++/Java 底层)。我当时就是按这个清单逐个模块复习,比漫无目的刷题有效得多。
第二,笔试看准确率,面试看思维过程。同样是编码题,笔试时你写出一个能跑通所有用例的完整解法就能拿高分,但面试时面试官更关注你如何分析问题、怎么处理边界条件、有没有考虑复杂度和并发安全。所以准备笔试时,先保证熟练度;如果进入面试环节,再刻意练习“讲思路”。
第三,老题也有价值。2016 年的题目虽然年代久远,但核心考点并没有过时。尤其是语言底层和操作系统部分,现在面试依然在问,只是换了一层皮。把一套经典笔试题吃透,远比走马观花刷十套新题更有用。
如果你正在准备网易或其他公司的研发岗笔试,可以按这套思路做一次复盘:先独立做完,再对着答案分析每道题背后的知识点,最后把错题归类。我当时整理了一个错题本,考前只看本子,效率很高。希望这篇复盘能帮你少踩一些我当年踩过的坑。