已经过去这么多年,百度2016年的研发工程师笔试题(一)依然在不少技术社群里被反复翻出来。很多人在牛客网、CSDN或者GitHub的面经仓库里刷过这套题,它看起来只是“一套选择题为主、夹杂少量编程题的笔试卷”,但真正刷完一遍之后你会发现,这套题几乎把所有研发岗必须过一遍的基础盘都盖住了:算法、数据结构、操作系统、网络、C++、面向对象,样样都有。
我当年刷这套题时是抱着“查漏补缺”的心态去的,结果被自己的知识盲区敲了一记闷棍:很多知识点上课时觉得自己懂了,真拿到题里一做就露馅。这篇文章我会按这套题的常见考点和出题逻辑来复盘,把选择题背后的原理、算法题的思路、C++和OS里容易踩的坑逐个拆开讲。无论你是正在准备校招的应届生,还是工作几年想回头补基础的开发,这篇文章都能当一份“带注释的复习提纲”用。
1. 这套题到底在考什么
1.1 试卷结构与考察逻辑
2016年的这套笔试题(一)整体上分成两个大部分:一部分是客观选择题,覆盖数据结构、操作系统、计算机网络、C++语言特性;另一部分是算法和逻辑题,需要通过手写代码或者给出详细的解题思路来回答。从我在牛客网和一些考研论坛上看到的回忆版来看,题目总数在二十道上下,选择题占了大头,最后会给一两道需要动笔的算法题。
很多人一看“一套旧题”就不屑于刷,觉得几年过去题型早变了。但实际上,大厂笔试的考察框架非常稳定。算法题永远是链表、树、栈、队列、动态规划;选择题永远是进程线程、内存管理、TCP、虚函数、指针。变化的只是包装场景和选项细节。2016年的题目尤其典型,它没有特别出格的偏题怪题,几乎所有题目都属于“基础概念+一两步推理”的组合,能在规定时间内把正确率稳定在八成以上,说明基础已经相当扎实了。
1.2 为什么2016年的题现在还能刷
有一个很现实的原因:现在的笔试题库虽然越来越大,但很多新题的题源就是早年这些经典题。面试官喜欢把旧题改个参数、换层壳,内核一点没变。比如栈的出栈序列、二叉树的前中后序遍历还原、哲学家就餐问题的变体,我在后几年的面试和带人过程中,几乎每年都能见到。
另一个原因是这套题的难度梯度很合理。它的选择题不是“一眼看穿”的送分题,也不是“绞尽脑汁也猜不出来”的炫技题。每道题基本都给了你三到四个选项,其中故意放了一两个特别像正确答案的干扰项。这种设计特别适合用来训练“对概念的精确理解”——你不但要知道某个知识点是什么,还得知道它什么时候是对的、什么时候会悄悄变成错的那个。这种能力恰好是实际开发中最需要的。
2. 算法与数据结构:送分题和送命题
2.1 栈和队列:出栈序列与单调栈
栈和队列是笔试题的常客,几乎每套试卷里都会有一道和“出栈序列”相关的题。题目一般长这个样子:入栈顺序为1、2、3、4、5,下列哪个出栈顺序是不可能实现的?
这类题我给个笨办法,也是我后来教给周围人的办法:不要一个选项一个选项去模拟,直接把每个选项按“入栈严格递增”的规则去验证。规则就两条:出栈时,比栈顶大的元素一定已经入栈;比栈顶小的元素,必须是当前栈顶到栈底的顺序。换句话说,如果某个元素在出栈序列中出现在另一个比它小的元素之前,那么它在入栈时一定先于那个元素入栈,但它又不能在入栈顺序上违反规律。
当年很多人在这种题上失分,不是不会栈的原理,而是一道题模拟太久,浪费了大量时间。后来我养成一个习惯:遇到出栈序列题,先从答案里找“第一个元素最小/最大”的极端情况,往往能快速排除两个错误选项。比如入栈序列1到5,出栈序列第一个如果是5,那说明五个元素全入栈了,之后只能按5、4、3、2、1的顺序弹;第一个如果是1,那就是入一个弹一个,后面元素要按规则判断。
2.2 二叉树遍历与递归转迭代
二叉树的前序、中序、后序遍历,绝对是这套题的重头戏。我记得有一道经典题是给一个二叉树的前序遍历序列和中序遍历序列,要求推后序遍历。这道题考的不只是“会做”,更考“会不会利用前序找根、利用中序分左右子树”的递归思想。
我建议所有准备笔试的人,哪怕你现在已经工作了,都重新动笔写一遍“递归转非递归”的三个遍历。中序遍历的非递归写法是最容易出错的,因为它需要用一个指针做“沿着左子树一路压栈”的动作,弹栈之后再转向右子树。很多人写着写着就把“转向右子树”写成了“压右子树然后继续压左子树”,导致出现了重复访问。
另外要注意边界条件:根节点是空指针、只有一个节点、所有节点都只有左子树或者只有右子树。笔试的算法题不会只有正例,它的测试用例一定会包含这些边界情况。我自己刷题时会把树相关的代码写在白纸上,模拟跑几组数据,写完之后再看一遍有没有“空指针解引用”的风险。这个习惯到现在写生产代码都还在用。
2.3 动态规划与贪心怎么区分
这套题的算法大题里经常出现一个“最大子数组和”或者“最长公共子序列”的原型。题目本身不难,但很能看出你是真的理解DP还是只会背模板。
我见过不少人在这一类题上分不清贪心和动态规划。简单区分:贪心是每一步都做当前看起来最优的选择,不能回头;DP则是把问题拆成重叠子问题,把中间结果存下来,之后不断复用。最大子数组和这样的一维DP,状态转移方程为dp[i]=max(nums[i], dp[i-1]+nums[i]),本质上就解决了两件事:要么从当前元素重新开始,要么把当前元素接到之前的最大和后面。你在纸上画几个例子就会发现,这个转移逻辑是“最优子结构”的体现。
笔试里还有一类很容易失分的题,是“硬币找零最少个数”。这种题有人会用贪心去解,但贪心只有在硬币面额满足特定条件时才成立。比如面额是1、5、11,要找15,贪心会先拿一个11,再拿四个1,一共5枚;但正确的最优解是三个5,只要3枚。这就是为什么笔试考官喜欢用这种题来试探你对DP到底有没有真正的理解。
3. 操作系统与网络:选择题里面的拦路虎
3.1 进程线程、锁和死锁
操作系统部分对很多刷题的人来说是“软肋”,因为平时写业务代码很少直接碰这些概念,但笔试偏偏特别喜欢考。2016年的这套题里,进程和线程的区分是必考的,而且一般会从这几个角度来出题:谁拥有独立地址空间、谁可以共享全局变量、线程调度开销小在哪里、进程切换为什么比线程切换慢。
我总结了一个方便记忆的口诀:进程是资源分配的基本单位,线程是CPU调度的基本单位;同进程下的线程共享地址空间、文件描述符、信号处理函数,但每个线程有自己的栈、寄存器和程序计数器。凡是在选项里说“线程拥有独立地址空间”的,直接排除。
死锁这道题也几乎是固定出场。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。选择题经常把事情倒过来问,比如“破坏循环等待条件就能破坏死锁”对不对?答案是肯定的,要么给资源编号、按序申请,要么一次性申请全部资源。但要注意“互斥条件”在很多场景下是不能破坏的,因为资源的互斥性是由使用场景决定的,不是写代码想改就能改。
3.2 内存分配与页面置换
内存管理这块,选择题爱考的是:分页和分段到底有什么区别、虚拟内存是靠什么支撑的、页面置换算法哪个缺页次数最少。
我当年总把分页和分段弄混。后来我用一句话记住:分页是系统为了管理物理内存而产生的,对程序员透明,页面大小固定;分段是程序为了满足逻辑模块而产生的,段大小不固定,程序员能看得到。从地址转换来看,分页用页号和页内偏移,分段用段号和段内偏移。
页面置换算法里,LRU和FIFO是最常见的两个比较对象。LRU的理论缺页率通常比FIFO低,因为LRU利用了局部性原理——最近访问过的页面短期内大概率还会被访问。但注意,笔试里经常给一个很小的访问序列,让你手动模拟两种算法的缺页情况,这时候LRU并不一定永远比FIFO少,如果序列里恰好频繁访问“刚被换进来”的老页面,FIFO可能碰巧表现更好。刷题的时候别背结论,老老实实把过程画出来。
3.3 TCP握手、拥塞控制与HTTP状态码
网络部分的经典三连问:TCP三次握手为什么是三次、四次挥手为什么是四次、TIME_WAIT出现在哪一端。
三次握手的核心原因是防止“已失效的连接请求报文段突然又传到了服务器”,如果没有第三次确认,服务器会白白建立连接、分配资源。四次挥手比三次握手多一次,是因为TCP半关闭的特性:一端发送FIN表示我这边不再发数据了,但还可以收数据;另一端先回ACK表示我收到你的FIN,然后再发送自己的FIN表示我也要关了。
HTTP状态码的考察也是重头戏。2xx是成功,3xx是重定向,4xx是客户端错误,5xx是服务端错误。细节陷阱在于301和302的区别:301是永久重定向,302是临时重定向,浏览器对301的缓存策略和对302完全不一样。还有403和404的区别:403是服务器理解请求但拒绝执行,404是资源不存在。
4. C++基础和面向对象:这里全是细节
4.1 指针引用与内存三大问题
C++类型的题在这套卷子里占比很高,而且往往不是考语法本身,而是考生命周期和内存布局。有一个比较典型的题目是问“引用和指针的区别”或者“sizeof(指针)是多少”,这类题看似基础,踩坑率极高。
指针和引用的关键区别:引用必须在定义时初始化、一旦绑定不能改指其他对象、没有空引用;指针可以随时指向其他对象,也可以为空。所以“用一个引用去接收一个临时变量”行不行,答案是“非常量引用不行,常量引用可以”,因为非常量引用会阻止临时变量的生命周期延长,编译器直接报错。
内存问题无非是三大类:悬空指针、内存泄露、数组越界。其中悬空指针最隐蔽,尤其是“返回指向局部变量的指针”“delete之后没有置空”这两种情况。写选择题时,看到“delete后指针自动变成空指针”这种描述,一定要打上大大的叉。
4.2 static、const、volatile这些修饰符
C++选择题里,static和const的考察密度非常高。static在类里修饰成员变量,表示所有对象共享一份数据,必须在类外初始化;static修饰成员函数,表示该函数不依赖具体对象,且只能访问静态成员。const修饰成员函数,表示该函数不会修改对象状态,而一个const对象只能调用const成员函数。
volatile是另一个高频考点。它的作用是告诉编译器不要把这个变量优化到寄存器里,每次使用都从内存重新读取。实际开发中,被外部中断服务函数修改的全局变量、多线程间共享的硬件寄存器标志位,都需要加volatile。但要注意,volatile不能保证线程安全,它只解决“编译器优化导致读取不到最新值”的问题。
这四个修饰符单独考都不难,难的是排列组合:static const、const static、const成员函数+static成员变量、volatile指针和指针指向volatile。重点就是注意修饰的是“指针本身”还是“指针指向的对象”——int const *p和int *const p是两个完全不同的声明,这种题目基本每年都会出现。
4.3 继承多态与虚函数表
面向对象部分一定会考多态的实现原理。知道“virtual关键字让函数实现动态绑定”只是最低要求,更重要的是理解虚函数表。每个包含虚函数的类,在编译期会生成一张虚函数表,表里保存的就是虚函数地址;对象内存里第一个指针(vptr)指向这张表。当用基类指针调用虚函数时,实际调用的是运行时对象类型对应的虚函数表里的函数地址。
有一道经典题是“构造函数和析构函数里调用虚函数会怎么样”。答案是:构造函数中调用虚函数不会发生多态,调用的还是当前类自己的版本;析构函数同理。原因是构造对象时先构造基类部分,此时还没有派生类成员,虚函数表也还处于基类阶段;析构时先析构派生类部分,再析构基类部分,等执行到基类析构函数时,派生类信息已经没了。
5. 实战复盘:我刷这套题踩过的坑
5.1 时间分配与答题顺序建议
这套题的选择题数量不少,纯做题可能三十分钟就能完成,但如果要把每一道题都当成一次“查漏补缺”的机会,我建议至少留出一个半小时。我的刷题方式是先不做任何笔记,快速过一遍,把不确定的题目标记出来;第二遍再单独处理这些标记题;最后再回头分析,为什么第一次会犹豫。
做题顺序上,我建议先做算法题,再做OS和网络选择,最后做C++部分。算法题需要大块时间和清晰的思维状态,放在选择题前面写,能避免到了后半程体力下降导致丢分。C++题再怎么说也是选择题,可以先靠直觉选一遍,回头再仔细推敲。
5.2 失分重灾区清单
根据我刷完这套题以及帮别人复盘的经验,失分点集中在这么几个地方:
| 失分点 | 常见原因 | 应对策略 |
|---|---|---|
| 出栈序列判断错误 | 只凭感觉模拟,不写规则 | 用好“先入先出约束”逐项验证 |
| 二叉树递归转非递归 | 中序和后序的入栈时机混淆 | 用栈状态机画三遍,记住“左、根、右” |
| 进程线程概念混淆 | 忽略“地址空间”和“调度单位”的区别 | 背口诀:进程管资源,线程管运行 |
| 死锁条件记不全 | 只记住三个就以为万事大吉 | 写代码时主动检查四个条件 |
| TCP握手次数原理 | 机械记忆“三次”,不理解为什么要三次 | 模拟“两次握手导致资源浪费”场景 |
| 虚函数调用时机 | 不知道构造/析构中的动态绑定失效 | 明确对象生命周期各阶段的状态 |
| const和引用细节 | 混淆“指向的对象”和“指针本身” | 用右左法则逐层解析类型声明 |
5.3 把一套题提炼成自己的知识模板
刷完一套题最重要的产出不是“我做了20道题”,而是一份属于自己的知识清单。拿这套题来说,我会在题目旁边标注它属于哪个知识域,然后给每个知识域再补充一道自己找的拓展题。这样处理完一套题,等于把整个知识树重新梳理了一遍。
比如,看到二叉树遍历题,我就在旁边写“还需要再看:已知中序+后序还原二叉树、Morris遍历、层序遍历的锯齿形输出”。看到进程线程题,就写“还需要再看:协程与线程的关系、线程池的参数设计、锁的几种实现”。这套题就像一张地图,它告诉你地图上哪些格子已经被考遍了,剩下的格子要靠你自己去点亮。
6. 写在最后:面试题只是入口
这套2016年的笔试题我后来带了几届新人,反复拿出来当摸底卷子用。每次有候选人问我“刷这套题有没有用”,我都是同一个回答:有用,前提是你别把它当成题库,而是当成一面镜子。
我在实际带团队的过程中见过太多能把LeetCode刷三遍、但一问线程安全的本质就卡壳的候选人。反倒是那些愿意在这套2016年“老题”上慢慢磨的人,往往能把基础概念和工程实践串起来。就像当年我自己刷到“C++构造函数不能调用虚函数”这道题时,第一反应是“这题好偏”,后来真正在重构一个基类时,才发现如果基类构造函数里偷偷调用了虚函数,排查起来真的会让人极其崩溃。
如果你正准备校招或者跳槽,我的建议是把这套题放进一个三天的学习计划里:第一天完整做一遍,第二天把错题对应的知识点全部展开细看,第三天再拿一套同类型的新题做检验。不要贪多,一套题吃透比草草刷十套题管用得多。等你有一天面试别人,翻开这套题,能轻松说清楚为什么每个选项是对的或者错的,那说明你的计算机基础,已经真正站稳了。