1. 这套题到底在考什么先看懂百度2016研发岗的出题逻辑2016年的百度研发工程师笔试放到今天来看依然有很强的参考价值。虽然这几年各家的笔试题型变了不少开始向ACM风格、代码输出题靠拢但百度的这套“第五套”笔试题结构上非常典型选择题为主覆盖面广重点踩在C、数据结构、操作系统、计算机网络这几个计算机基础的硬骨头上。说白了这套题考的不是你写过多少业务代码而是你在学校或者自学阶段对那些“底层到不能再底层”的概念到底理解到什么程度。很多题目单看知识点并不稀奇比如虚函数、排序稳定性、二叉树遍历、进程调度但百度喜欢在这些基础概念上做文章在细节处设置陷阱。你要是只是“背过”而没有真正“理解过”做起来会非常难受。我当时刷这套题的第一感受是选择题题干很短但每个选项都值得反复琢磨。有些选项单独拿出来是对的放在题目语境里却是错的有些看起来是考概念实际上考的是你是否踩过某个具体的坑。这种出题风格其实就是研发岗日常工作的缩影——你需要在一堆“看起来都能跑”的方案里选出最合适、最不会出问题的那一个。这篇文章不是简单地贴一遍答案而是把我当年刷这套题时踩过的坑、查过的资料、后来在工作中验证过的理解一并整理出来。如果你正准备校招、跳槽大厂或者只是想把计算机基础重新打牢这套题值得花一个下午认真过一遍。2. 逐题拆解考题背后的知识点与判题逻辑2.1 C虚函数与多态的底层实现一个指针能挖多深这套题里有几道关于C虚函数的题目很典型。表面上是问“虚函数的作用是什么”“析构函数为什么要声明为虚函数”但真正想让你回答的是多态在底层是怎么实现的。C的多态之所以能跑起来靠的是一个叫虚函数表vtable的东西。每个包含虚函数的类在编译期会生成一张虚函数表表里存放的是这个类所有虚函数的地址。每个对象实例里编译器会偷偷塞进一个指针叫虚函数表指针vptr指向这个类对应的虚函数表。当通过基类指针调用虚函数时编译期不会直接生成“调用某个固定地址”的指令而是生成“先取出对象的vptr再到vptr指向的表里找对应偏移量的函数地址然后跳转执行”的指令。这个间接跳转就是多态的实现基础。我知道很多人看到这里会说“这我懂。”但下面这个问题才是关键为什么析构函数要声明为virtual假设有一个基类Base和派生类Derived。Derived里有一个指针成员指向堆上的一块内存。如果基类的析构函数不是虚函数那么当你写这样一段代码Base* p new Derived(); delete p;此时delete p只会调用Base的析构函数Derived的析构函数完全不会执行。这意味着Derived里那个指针成员指向的堆内存永远不会被释放直接内存泄漏。但是如果你把Base的析构函数声明为virtual情况就变了。delete p会先通过vptr查到Derived的析构函数地址先执行Derived的析构逻辑再自动调用Base的析构函数完成整条继承链的清理工作。这个考点在笔试题里通常以“以下说法正确的是”的形式出现。常见的错误选项是“析构函数声明为虚函数后派生类的析构函数也会自动变成虚函数”——这个说法其实是对的因为派生类会覆盖基类的虚析构函数另一个常见错误选项是“构造函数也可以声明为虚函数”——这是明显错误的因为对象还没构造出来vptr都还不存在拿什么去查虚函数表真正容易被忽略的坑是**虚函数表是在编译期生成的不是运行期。**运行期只是通过vptr去“查询”和“跳转”而表本身的结构在编译阶段就定好了。这也是为什么虚函数调用会有轻微的性能损耗——多了一次间接寻址但损耗在现代CPU预测技术下基本可以忽略只有极端性能敏感的场景比如高频小函数的虚调用才需要认真评估。2.2 排序算法的时间复杂度与稳定性你记得越熟越容易掉进陷阱这套题里必有一道关于排序算法的选择题而且通常不直接问“快排的时间复杂度是多少”而是问“以下哪个排序算法是稳定的”或者“哪个排序算法在平均情况下最快”。关于排序我先说一个最容易混淆的点稳定性到底是什么。排序算法的稳定性是指如果两个相等的元素在排序前的相对顺序是A在前B在后排序后A依然在B的前面那就说明这个排序算法是稳定的。举个例子。假设有一个学生列表先按班级排好序再按成绩排序。如果第二次排序是稳定的那么成绩相同的学生还是能保持按班级排好的相对顺序。如果第二次排序不稳定那成绩相同的学生可能会被打乱班级顺序就丢了。常见排序算法的稳定性结论算法平均时间复杂度最坏时间复杂度空间复杂度稳定吗冒泡排序O(n²)O(n²)O(1)稳定插入排序O(n²)O(n²)O(1)稳定选择排序O(n²)O(n²)O(1)不稳定希尔排序取决于增量序列O(n²)O(1)不稳定归并排序O(n log n)O(n log n)O(n)稳定快速排序O(n log n)O(n²)O(log n)不稳定堆排序O(n log n)O(n log n)O(1)不稳定我当时把这张表背得滚瓜烂熟但做题时还是栽了一次因为题目问的是“快速排序在什么情况下时间复杂度退化为O(n²)”。答案是当每次划分都选到最小或最大元素作为基准时也就是数组本身已经有序或基本有序时。你有没有发现一个悖论数组已经有序按理说是“最好”的情况但对快排来说却是“最坏”的情况。因为快排的基本策略是分治分治的效果取决于每次划分能否把数组切成“一大半加一小半”。如果每次只切掉一个元素递归深度就变成n时间复杂度自然退化成O(n²)。所以如果你看到一道题问“以下哪种情况快速排序最慢”答案不是“数组完全乱序”而是“数组已经有序或逆序”。这也解释了为什么实际工程中快排的实现都会加上“三数取中”或者“随机选择基准”的优化——目的就是尽量避免退化成O(n²)。2.3 二叉树的遍历序列给你前序和中序你能还原出树吗这套题里有一道很经典的二叉树题目给定前序遍历序列和中序遍历序列要求推导出后序遍历序列或者判断某个节点的左右孩子关系。这个知识点考的不是遍历本身而是你对“递归结构”的理解深度。因为二叉树的遍历序列背后是一个递归定义的逻辑结构。前序遍历的顺序是根节点、左子树、右子树。中序遍历的顺序是左子树、根节点、右子树。两者搭配能唯一确定一棵二叉树。道理很简单前序遍历的第一个节点一定是整棵树的根找到根之后在中序遍历序列里根左边的所有节点都属于左子树根右边的所有节点都属于右子树。然后递归地对左子树和右子树做同样的操作就能把整棵树还原出来。举个例子。前序遍历序列是A B D E C F中序遍历序列是D B E A F C。第一步前序的第一个节点是A所以A是根。在中序序列里A左边是D B E右边是F C。所以左子树的中序序列是DBE右子树的中序序列是FC。第二步前序序列里左子树的节点对应的是B D EA后面的三个节点前序的第一个是B所以B是左子树的根。在中序序列DBE里B左边是D右边是E所以D是B的左孩子E是B的右孩子。第三步右子树的中序序列是FC前序序列里右子树对应的是C F左子树三个节点之后前序第一个是C所以C是右子树的根。在中序序列FC里C左边是F所以F是C的左孩子。最终树的结构是A / \ B C / \ / D E F后序遍历就是D E B F C A。这个考点笔试题通常会给你一组序列让你选“哪个后序遍历是正确的”或者“哪个选项不可能是这个树的遍历结果”。第二种问法更难因为它需要你对序列的结构约束有深刻理解——比如前序遍历的第一个节点必须在中序遍历序列里出现且左右子树的长度必须匹配。我自己的经验是千万不要在脑子里硬推考试时在草稿纸上一层层画出来速度快而且不容易出错。平时练习时可以用代码把这个过程写一遍递归函数只需要十几行就能搞定。TreeNode* buildTree(vectorchar preorder, vectorchar inorder) { if (preorder.empty()) return nullptr; char rootVal preorder[0]; TreeNode* root new TreeNode(rootVal); int pos find(inorder.begin(), inorder.end(), rootVal) - inorder.begin(); vectorchar leftIn(inorder.begin(), inorder.begin() pos); vectorchar rightIn(inorder.begin() pos 1, inorder.end()); vectorchar leftPre(preorder.begin() 1, preorder.begin() 1 leftIn.size()); vectorchar rightPre(preorder.begin() 1 leftIn.size(), preorder.end()); root-left buildTree(leftPre, leftIn); root-right buildTree(rightPre, rightIn); return root; }练熟了这套递归逻辑二叉树遍历相关的笔试题基本都能秒杀。2.4 进程与线程的区别不只是“资源分配”和“调度”两个词百度这套题里有一道关于操作系统的题目问的是进程和线程的区别。大部分人都能答出“进程是资源分配的基本单位线程是CPU调度的基本单位”但题目会进一步追问进程之间为什么不能直接共享内存线程之间又为什么可以共享数据这里面的底层逻辑是每个进程有独立的虚拟地址空间也就是所谓的进程隔离。进程A的虚拟地址和进程B的虚拟地址在物理内存上可能完全不相关。操作系统通过页表把虚拟地址映射到物理地址而每个进程的页表是独立的。所以进程A里的某个指针放到进程B里去访问轻则访问到不属于它的内存重则触发段错误直接崩溃。线程则不一样。同一个进程里的多个线程共享同一个虚拟地址空间和页表。所以线程A里定义的一个全局变量线程B可以直接访问。这就是“线程之间数据共享容易”的根本原因。但是共享带来便利的同时也带来了新的问题并发访问冲突。两个线程同时对同一个变量执行自增操作最终结果可能不是2而是1。因为自增操作在CPU层面是“读值、加一、写回”三步两个线程可能在“读值”这一步都读到了0然后各自加一写回结果就是1。这种问题怎么解决靠同步机制互斥锁、读写锁、信号量、原子操作。笔试题目通常会问“以下哪种机制可以保证多个线程对共享变量操作的原子性”答案一般是原子操作或者互斥锁。我建议你把这张表记下来笔试高频维度进程线程资源分配进程是独立单元线程共享进程资源地址空间独立互不干扰共享可直接访问通信方式管道、消息队列、共享内存、Socket直接读写共享变量切换开销大需要切换页表、刷新TLB小只需切换寄存器上下文健壮性一个进程崩了不影响其他进程一个线程崩了整个进程都崩有一个点很多人不知道线程切换的开销小并不意味着线程切换的开销可以忽略。频繁创建和销毁线程同样会带来性能问题所以才有了线程池这种东西。笔试题如果问“为什么高并发场景下使用线程池”答案不是“创建线程慢”这么简单更准确的表述是创建线程需要向操作系统申请内核资源销毁线程需要释放内核资源这些操作涉及用户态和内核态的切换成本很高。线程池的本质是“复用”把创建和销毁的开销平摊到多次任务执行上。2.5 计算机网络里的TCP三次握手与四次挥手百度百考不厌的送分题2016年的这套题网络部分基本绕不开TCP协议。考得最多的就是三次握手和四次挥手但这道题同样是“最容易丢分”的题因为大多数人只背了流程没理解状态变化。三次握手的流程我必须再写一遍因为太重要了客户端发送SYN报文其中SYN1seqx进入SYN_SENT状态。服务端收到SYN报文回复SYNACK报文其中SYN1ACK1seqyackx1进入SYN_RCVD状态。客户端收到SYNACK报文回复ACK报文其中ACK1seqx1acky1进入ESTABLISHED状态。服务端收到后也进入ESTABLISHED状态。笔试常见的考法有两种。第一种是问“第二次握手时服务端发送的ACK值是多少”答案是客户端初始序列号加一也就是x1。第二种是问“为什么是三次而不是两次或四次”。关于为什么不能是两次有一个很经典的场景客户端发送了一个SYN报文因为网络拥塞滞留了客户端超时重传了一个SYN报文服务端收到重传报文建立连接并完成数据传输后关闭了连接。这时候之前滞留的那个SYN报文才到达服务端。如果只有两次握手服务端收到这个迟到报文后会直接建立连接白白浪费资源。而三次握手可以解决这个问题——服务端收到迟到的SYN后会回复SYNACK客户端发现自己并没有发起过新的连接请求会回复RST报文服务端收到RST后就知道这个连接请求是废弃的从而不建立连接。四次挥手稍微复杂一些。过程是主动关闭方发送FIN报文进入FIN_WAIT_1状态。被动关闭方收到FIN报文回复ACK报文进入CLOSE_WAIT状态。主动关闭方收到ACK后进入FIN_WAIT_2状态。被动关闭方发送完所有数据后发送FIN报文进入LAST_ACK状态。主动关闭方收到FIN报文后回复ACK报文进入TIME_WAIT状态。被动关闭方收到ACK后进入CLOSED状态。TIME_WAIT持续2MSL后自动变为CLOSED。笔试题最爱问的是“为什么主动关闭方要进入TIME_WAIT状态并且等待2MSL”两个原因。第一保证被动关闭方收到了最后的ACK。如果这个ACK丢失了被动关闭方会重发FIN主动关闭方如果没有保持TIME_WAIT就收不到这个重发的FIN也就不会再发一次ACK被动关闭方就会一直卡在LAST_ACK状态。第二让网络中所有延迟的报文自然消亡。2MSL是报文在网络上存活的最长时间等待2MSL后本次连接产生的报文就已经全部消失在网络中不会干扰后续使用相同端口的新连接。这个考点工作之后如果排查过大量TIME_WAIT状态的连接理解会更深。高并发服务端短时间内处理大量短连接就可能出现TIME_WAIT堆积。面试时如果你能主动说出“TIME_WAIT占用少量端口资源但2MSL是为了防旧包串扰不能随意缩短”会加分不少。3. 这套题背后的出题意图百度到底在筛选什么样的人3.1 基础不牢代码写得再花也白搭如果你把2016年百度的研发笔试题全部过一遍会发现一个规律没有一道题是“纯刷题”能刷出来的偏怪难题目所有考点都来自计算机基础课程里的主干内容——C内存模型、数据结构、操作系统、计算机网络、数据库索引。这套题的设计逻辑很清晰先筛掉基础不牢的人再筛掉思维方式不对的人。比如C虚函数那道题为什么要一遍遍考因为多态是面向对象设计的核心机制你要是连虚函数表是什么都没理解就说明你对C这门语言的理解停留在“照着语法写”的层面。这类人写业务代码没问题但一旦遇到性能优化、内存泄漏、线上崩溃就会手足无措。再比如TCP三次握手很多从培训班出来的人能背得滚瓜烂熟但你要是追问“为什么客户端最后还要发一次ACK”立刻卡壳。这说明他只学到了“面试要考”的程度没有学到“底层机制要理解”的程度。百度的研发岗要的是第二种人。3.2 时间分配策略笔试不只是考你会不会还考你会不会取舍做过这套题的人普遍反映一个问题选择题量比较大而且每道题都要花时间读选项、排除干扰项。这就非常考验时间管理能力。我的建议是拿到试卷先花两分钟把所有题目通读一遍把“一眼就能确定答案”的题先做掉把“需要仔细计算”的题标记出来最后集中攻克。不要在某一题上死磕超过五分钟因为研发岗笔试通常还有后面的编程题或简答题分值占比往往不低。另外有一个技巧值得分享在做选择题时如果你能直接确定某个选项是错误的就可以用排除法。尤其是那种“以下说法错误的是”的题型先把明显正确的选项划掉正确答案往往会浮出水面。但要注意有些题目会故意把两个选项设计得非常接近比如“虚函数表是运行期生成的”和“虚函数表是编译期生成的”——这两个说法只有一个是对的。如果你不确定优先从底层机制推导而不是凭记忆猜。3.3 从笔试到面试一道选择题可以延伸出一场追问这套题还有一个隐藏价值它对应的面试追问方向非常明确。我见过不少面试官会拿笔试里你做错的题目当场复盘连续追问四五个“为什么”。比如你笔试时选错了“快速排序最坏情况”的答案面试官会接着问“那你有没有什么办法避免快排退化成O(n²)”“三数取中的原理是什么”“STL里的sort是怎么实现的为什么它不直接用快排”这些追问本质上就是在一步步测试你的知识边界。你能答到哪一层就说明你对这个知识点的理解深度在哪一层。所以刷这套题的时候不要只满足于“选对了答案”而是要把每道题都当成一个知识树的分支顺着它往下挖。这里给你一个我自己的复盘模板这道题考的是哪个基础知识点我为什么选错了是概念模糊还是掉进了陷阱选项如果别人拿这道题来面试我他会往下追问什么追问的问题我现在能答出来吗答不出来的立刻去查资料补上。用这个模板过一遍这套题收获会比单纯刷题大得多。4. 复盘清单与经验总结如何高效利用这套题4.1 做一次“错题归因”比做十套新题更有用我刷完这套题之后最大的感受是错题往往不是败在不会而是败在“半懂不懂”。很多概念我看书的时候觉得懂了做题的时候才发现自己脑子里的模型是有偏差的。所以复盘时建议做一次错题归因把自己的错题分成三类记忆型错误概念记混了比如把堆排序和归并排序的稳定性记反了。这种错误背诵加默写就能解决。理解型错误知道概念但没有理解底层机制比如不知道TCP为什么要TIME_WAIT。这种错误需要把机制完整推导一遍直到自己能讲给别人听。粗心型错误审题不仔细比如题目问“以下说法错误的是”你看成了“以下说法正确的是”。这种错误只能靠做题时圈出关键词来避免。做完归因之后你就能清晰地看到自己在哪一类问题上丢分最多然后针对性补强。4.2 把每道题改造成面试题自问自答这套题特别适合用来做面试模拟。你可以把选择题里的一个个知识点改造成开放式的简述题“请从虚函数表的角度解释一下C多态的实现原理。”“快速排序什么时候会退化成O(n²)工程上如何避免”“进程和线程各自的开销来自哪里”“TCP三次握手为什么不能省掉最后一次ACK”注意这些问题不能只用自己的话复述一遍标准答案最好能结合一个实际场景来讲。比如讲多态的时候可以结合“为什么大型项目里虚函数过多了会轻微影响性能”讲进程和线程的时候可以结合“为什么Nginx用多进程模型而不是多线程模型”。这样练过之后你在面试里遇到类似的追问反应速度会明显不一样。4.3 这套题的局限与扩展别停留在2016年有一点必须提醒2016年的笔试题放到今天确实有些内容已经不够用了。比如分布式架构、容器化、云原生这些热门方向那套题里几乎没有涉及。但这并不意味着这套题过时了而是说你可以把它当作“基础层”的检验工具在此基础上再去补充“应用层”的新知识。我的排序思路是基础知识层用这套题检验C、数据结构、操作系统、网络的掌握程度。编码能力层去LeetCode上刷高频题重点练动态规划、贪心、图论、字符串处理。系统设计层阅读高并发系统相关的博客和书籍理解缓存、消息队列、分布式一致性等问题。项目实战层用真实项目把前面积累的知识串起来形成自己的技术判断力。每一步都要在前一步基本过关之后再进行。否则你连二叉树遍历都写不利索去啃分布式系统大概率是浪费时间的。最后再分享一个小技巧刷这套题的时候给自己限时模拟真实的笔试环境。我当时是设定60分钟做完所有选择题时间一到立刻停笔然后对答案。这么做的好处是能真实反映出你在时间压力下的决策质量而不是在无限制的时间里慢慢推理。这套题我前前后后刷了三遍。第一遍惨不忍睹第二遍勉强及格第三遍能看着题目直接讲出考点和出题意图。三遍下来比刷十套新题收获都要大。希望你也一样。