
秋招季又到了后台一直有人问我非科班转码想冲大厂研发岗刷题到底从哪套开始比较有价值。我一般会建议把历年大厂笔试题拆开来做其中百度2016研发工程师笔试题二这套卷子虽然过去了很多年题量和难度放在今天依然有很强的参照意义。它不是那种偏题怪题堆起来的试卷考的大部分是研发工程师日常真正会用到的基础能力数据结构、算法复杂度、操作系统、网络协议外加一两道现场写代码的编程题。适合正在准备校招、社招换坑的研发同学也适合团队带新人时拿来摸底。我最近把它拆开重新做了一遍结合当年踩过的那些坑把值得展开的内容整理出来。1. 这套题到底在考什么整体画像与考点地图1.1 研发岗笔试为什么总考这些“老”东西很多同学第一次做大厂笔试题时都会有疑问我都准备做项目了为什么笔试还在考数组、链表、二叉树笔试和面试的定位不一样校招阶段绝大多数候选人没有拿得出手的线上项目经历简历上写的东西也很难在短时间内验证含金量。笔试就承担了一个“基础能力筛子”的角色它不关心你背了多少框架而是看你的代码功底、算法思维和计算机基础是否扎实这些恰恰是后续能不能独立扛需求、快速排查问题的底层能力。百度2016研发工程师笔试题二也是这个逻辑。整份卷子覆盖了数据结构、算法、C/C/Java语言基础、操作系统、计算机网络、Linux基础等方向选择题占大头中间夹着几道需要推导和计算的主观题最后是一到两道编程题。难点不在于每一题有多深而在于范围广、时间紧很多知识点如果你平时只靠“背八股”而没有真正写过代码考场上很容易卡壳。1.2 考点权重与题型分布先看清楚拿到任何一套笔试题我建议第一件事不是埋头做题而是先把题量和题型扫一遍规划一下时间。这套卷子的考点大致可以分成下面几块考点方向大致占比典型考查内容数据结构与算法40%数组、链表、二叉树、排序、动态规划、字符串处理C/C/Java语言基础20%指针、引用、虚函数、内存管理、STL容器操作系统15%进程与线程、死锁、内存分配、调度策略计算机网络15%TCP握手、拥塞控制、HTTP状态码、DNSLinux与其他10%常用命令、编译链接、设计模式、概率题从分布上能看出一个明显倾向算法和数据结构是绝对核心语言基础紧随其后。这意味着复习时不能只刷LeetCode语言层面的细节也要花时间过一遍尤其是C方向的同学指针和内存相关的题几乎是送分题但如果基础不牢就会变成送命题。后面我按这个权重把代表性题目逐个拆开讲。2. 数组与字符串高频题的复杂度陷阱2.1 三个数的最大乘积sort之后别急着交这道题在笔试里出现的频率非常高几乎可以称为大厂入门必考。题目描述很简洁给定一个整数数组数组里可能有正数、负数和零要求找出三个数使它们的乘积最大输出这个最大乘积。很多人的第一反应就是把数组排序然后取最大的三个数相乘。这个思路在数组全为正数时没问题但一旦出现负数就会踩坑。举个例子数组是 [-100, -98, 1, 2, 3]最大的三个数是 1、2、3乘积是 6但实际上两个负数相乘得到一个很大的正数-100 乘以 -98 再乘以 3结果是 29400比 6 大得多。所以正确解法不是只看最大三个数还要看“最小的两个数”和“最大的一个数”的乘积。排序的做法是先排序然后在a[n-3] * a[n-2] * a[n-1]和a[0] * a[1] * a[n-1]里取较大值排序的时间复杂度是 O(nlogn)。如果面试官要求 O(n)可以用一次扫描同时维护最大的三个数和最小的两个数核心代码如下int maximumProduct(vectorint nums) { int max1 INT_MIN, max2 INT_MIN, max3 INT_MIN; int min1 INT_MAX, min2 INT_MAX; for (int x : nums) { if (x max1) { max3 max2; max2 max1; max1 x; } else if (x max2) { max3 max2; max2 x; } else if (x max3) { max3 x; } if (x min1) { min2 min1; min1 x; } else if (x min2) { min2 x; } } return max(max1 * max2 * max3, min1 * min2 * max1); }这道题给我的最大启示是遇到“最大”“最小”这类题先想想有没有负数和零有没有可能通过符号变化改变结果。笔试不考奇技淫巧考的就是你考虑边界是否周全。2.2 最长回文子串从暴力到中心扩展字符串题在研发岗笔试里几乎不会缺席最长回文子串就是其中最有代表性的一道。回文串就是正着读和倒着读一样的字符串比如 aba、abba。题目一般要求输出给定字符串的最长回文子串长度或者直接输出子串本身。暴力解法是枚举所有子串再逐个判断是不是回文枚举子串是 O(n^2) 种每次判断是 O(n)总复杂度 O(n^3)笔试数据一大必挂。一个更好的思路是中心扩展法回文串有一个特点它是“从中间向两边对称”的所以我们可以枚举每一个可能的中点然后向两边扩展直到两侧字符不同为止。中点有两种形态长度为奇数的回文中心是一个字符长度为偶数的回文中心是两个字符之间的位置。string longestPalindrome(string s) { int n s.size(), start 0, maxLen 1; auto expand [](int left, int right) { while (left 0 right n s[left] s[right]) { left--; right; } return right - left - 1; }; for (int i 0; i n; i) { int len1 expand(i, i); // 奇数 int len2 expand(i, i 1); // 偶数 int cur max(len1, len2); if (cur maxLen) { maxLen cur; start i - (cur - 1) / 2; } } return s.substr(start, maxLen); }中心扩展法的时间复杂度是 O(n^2)空间复杂度 O(1)笔试中完全够用。这道题还有一个更优的 Manacher 算法可以做到 O(n)但它的实现比较绕笔试时如果时间紧不建议现场推导知道思路即可。在复盘时我反而建议大家把中心扩展法写到滚瓜烂熟因为它在面试现场拓展出“回文子序列”“回文子串个数”等问题时是最灵活的底子。3. 动态规划状态设计才是核心3.1 最长递增子序列O(n^2)与O(nlogn)两条路动态规划那几道题里最长递增子序列LIS是我个人最推荐重点刷的因为它在笔试和面试里都太常出现了。题目是这样给定一个无序数组求其中最长的严格递增子序列的长度。注意“子序列”不要求连续比如数组 [10, 9, 2, 5, 3, 7, 101, 18]最长递增子序列是 [2, 3, 7, 101] 或者 [2, 5, 7, 101]长度都是 4。第一反应通常是动态规划。定义dp[i]表示以第 i 个元素结尾的最长递增子序列长度那么状态转移方程比较好写dp[i] max(dp[j] 1)其中0 j i且nums[j] nums[i]。初始每个dp[i] 1最终答案取整个 dp 数组的最大值。这个思路的复杂度是 O(n^2)写起来快适合笔试时间紧张时保底。但如果数据量到 10^5 级别O(n^2) 就会超时这时需要换思路。O(nlogn) 的解法很巧妙维护一个数组tailstails[k]表示长度为 k1 的递增子序列中末尾数字最小的那个值。遍历原数组时对每个数在tails里做二分查找找到第一个不小于它的位置并替换如果找不到就追加。这个替换操作看起来只是在更新数字实际上是在贪心地保留更小的末尾值以便后续能接上更长的子序列。代码也不复杂int lengthOfLIS(vectorint nums) { vectorint tails; for (int x : nums) { auto it lower_bound(tails.begin(), tails.end(), x); if (it tails.end()) tails.push_back(x); else *it x; } return tails.size(); }这里要特别提醒一个误区tails数组本身并不是最长递增子序列它只是用来记录长度的工具。很多人复盘时盯着tails的中间状态想当然地认为它就是最终子序列结果面试被追问就露馅。笔试时候如果题目只要求输出长度这个解法是最优的如果要输出具体子序列需要额外记录前驱复杂度也会更高做题前一定要先看清题目要求。3.2 最小路径和从递归到记忆化到DP网格类动态规划是笔试编程题里另一个高频方向。典型题目给定一个 m x n 的网格 grid每个格子里放着一个非负整数找出一条从左上角到右下角的路径每次只能向右或向下移动求路径上所有数字之和的最小值。我第一次看到这题时第一反应是递归定义一个函数f(i, j)表示从起点走到(i, j)的最小路径和当前格子的值加上上一格左边或上边的较小值即可f(i, j) grid[i][j] min(f(i-1, j), f(i, j-1))递归写法非常直观但直接提交会超时因为存在大量重复计算。比如f(2,2)会被f(3,2)和f(2,3)各算一遍指数级膨胀。解决办法是加一个备忘录把已经算过的结果存下来这就是记忆化搜索。再往下走一步其实可以从左上角开始按行按列递推每个格子只依赖左边和上边的结果这就是标准的动态规划。因为每个格子只需要保留当前行和上一行的值所以还能用滚动数组把空间压到 O(n)。这是笔试中一个非常漂亮的优化点面试官问“能不能降低空间复杂度”时能答出滚动数组是加分项。int minPathSum(vectorvectorint grid) { int m grid.size(), n grid[0].size(); vectorint dp(n, INT_MAX); dp[0] 0; for (int i 0; i m; i) { for (int j 0; j n; j) { if (j 0) dp[j] grid[i][j]; else dp[j] grid[i][j] min(dp[j], dp[j - 1]); } } return dp[n - 1]; }复盘这套题时我发现一个规律笔试里的动态规划题考得难的不是状态转移方程本身而是你能不能快速定义出合适的状态。面试官不期待你一次性写出最优化解而是希望看到你从递归暴力开始逐步分析重复子问题再演进到记忆化、再优化空间这条完整的思考链路。所以练题的时候不要只背模板要把每一步为什么这么推想明白。4. 操作系统与网络看似送分实际送命4.1 进程与线程不是背定义而是看场景操作系统相关的选择题在百度这套笔试题里占了不小比例其中最基础也最容易翻车的就是进程和线程。很多人只背了“进程是资源分配的基本单位线程是CPU调度的基本单位”但真到做题时一换场景就懵。笔试常考的有几个点。第一进程和线程谁拥有独立的地址空间进程有线程没有线程共享所属进程的地址空间。所以一个线程崩了整个进程可能跟着崩而进程之间崩溃互不影响。第二切换代价谁更大进程切换需要切换地址空间开销大线程切换主要保存线程上下文开销小。第三进程之间如何通信管道、消息队列、共享内存、信号量、Socket重点要能说出共享内存为什么最快因为它不需要拷贝数据。第四线程之间同步有哪些手段互斥锁、条件变量、信号量、读写锁。我踩过的坑是只背概念没理解场景。比如有一道选择题问“多线程并发访问同一个全局变量可能出现什么问题”答案是数据竞争需要加锁保护。但如果不加锁是不是一定会出问题不一定因为要看具体平台和原子性CPU 底层如果对该变量的读写是原子的可能碰巧没问题但这属于未定义行为绝不能依赖。这种“看似正确但实际有隐患”的选项是笔试里用来筛人的惯用手段做题时遇到“一定”“必然”“绝对不会”这类绝对化表述要格外小心。4.2 TCP握手与拥塞控制网络题最爱挖的坑计算机网络在研发岗笔试里也是必考TCP协议又是重中之重。三次握手的基本流程大家都会背客户端发 SYN服务端回 SYNACK客户端再回 ACK。但笔试不考流程考的是为什么。“为什么不能是两次握手”这个问题很多同学答不完整。核心原因是防止历史连接请求突然又到达服务端导致服务端建立无效连接。假设只有两次握手客户端发了 SYN但没有收到响应超时后重发 SYN。此时旧 SYN 在网络中滞留服务端收到后以为是新连接返回 ACK连接就建立了但客户端根本没有发新请求这个连接就是浪费的。三次握手可以让服务端收到客户端第二次 ACK 时确认“这是上次请求还是新请求”从而规避这个问题。四次挥手里的 TIMEWAIT 也经常考。主动关闭方在发送最后一次 ACK 后要等待 2MSL 才真正关闭原因一是保证最后一个 ACK 能到对方手里如果丢了对方会重发 FIN原因二是让旧连接中的迟到达数据包在网络中自然消失避免影响下一个四元组相同的连接。很多同学只背了“2MSL”这个数字解释不出这两个原因被追问就露馅。拥塞控制这里慢启动、拥塞避免、快重传、快恢复四个阶段要分清尤其注意流量控制和拥塞控制的区别。流量控制是发送方根据接收方窗口大小调整发送速率解决的是“接收方来不及处理”的问题拥塞控制是发送方根据网络状态调整速率解决的是“链路太拥堵”的问题千万别混在一起。5. 语言基础与内存C/Java的常见陷阱5.1 指针与引用语法绕但本质简单C 方向的笔试题里指针和引用几乎每年都出现而且特别喜欢考复杂声明。最常见的题型是区分const char* p、char* const p和const char* const p。诀窍其实很简单从右往左读const修饰它左边的符号如果左边没有则修饰右边。const char* pp 是一个指向 const char 的指针指向的字符不能改但 p 本身可以改可以指向别的字符。char* const pp 本身是 const即指针变量不能被重新赋值但它指向的字符可以改。const char* const p两者都不能改。笔试里给出代码问“哪一行编译报错”时用这个规则扫一遍就行。还有一个高频对比题指针和引用的区别。引用必须在定义时初始化、不能为空、不能改变绑定对象而指针可以。底层实现上引用通常也是用指针实现的但是在语法层面它被设计成一个变量的别名。正因为引用不能为空函数传参时如果参数不允许为空优先用引用如果参数可能为空用指针更合适。5.2 内存分配与智能指针光会 new 不够C 内存相关的选择题花样很多核心集中在 malloc/free 和 new/delete 的区别、内存泄漏的原因、以及智能指针的使用场景。malloc 和 new 的本质区别有三个层次一是 malloc 只分配内存new 还要调用构造函数free 和 delete 对应地还要调用析构函数二是 malloc 返回 void* 需要强转new 返回具体类型指针三是 new 失败会抛异常malloc 失败返回 NULL。智能指针现在是面试高频点。unique_ptr独占所有权拷贝被禁止移动可以转移所有权shared_ptr允许多个指针共享对象内部用引用计数维护生命周期weak_ptr是配合shared_ptr使用只观察不拥有避免循环引用。有一道经典题问shared_ptr是不是线程安全的答案是引用计数本身是原子操作是线程安全的但指向的对象的读写不是线程安全的。也就是说多个线程同时拷贝同一个shared_ptr没问题但如果多个线程同时通过它修改对象的数据仍然需要加锁。很多人答“是线程安全的”就掉坑了。Java 方向的同学重点看 GCRoots 和垃圾回收算法笔试常考哪些对象可以作为 GCRoots虚拟机栈中引用的对象、方法区中类的静态属性引用的对象、常量引用的对象、本地方法栈中 JNI 引用的对象。这个知识点没有捷径需要理解 JVM 在判定对象存活时的遍历起点。6. 编程题实战一道题从读题到AC的全流程6.1 读题与样例分析先别急着写这套卷子最后通常有一到两道编程题要求现场手写完整代码。很多同学考后复盘时发现丢分不是因为不会做而是没有把读题和自测当回事。我以“网格最小路径和”这道典型题为例演示一下拿到题目后应该怎么走完整流程。先读题圈出三个关键信息网格的大小是 m x n只能向右或向下走求路径和最小值。这三个信息决定了状态转移方程的方向。如果题目改成“可以上下左右走”那就要考虑 DFS 或图论算法解法完全不同。再看样例手动走一遍确认理解的规则和题目一致。6.2 从暴力递归到记忆化再到DP写代码要有递进实际写代码时不建议一上来就写最优解。先用递归表达状态转移哪怕慢也要保证逻辑正确。然后加备忘录做记忆化最后再改成迭代 DP 加滚动数组。这个递进过程在笔试时不一定全部写出来但思考过程应该是这个顺序因为只有从暴力出发才不容易漏掉边界。最小路径和的状态转移很简洁核心就一行当前格子的最小值等于当前值加左边和上边格子的较小值。要注意两个边界第一行只能从左边来第一列只能从上边来。如果统一用dp[j]作为当前行的状态数组更新时就要单独处理 j 0 的情况。我复盘时发现很多人丢分就丢在这一行边界条件的处理上。6.3 自测用例提交前必做的三件事很多笔试平台是提交后判分没有第二次机会所以自测习惯特别重要。至少准备三类用例第一个是正常用例覆盖算法的主流程第二个是边界用例比如只有一行、只有一列、数组长度为 1第三个是特殊值用例比如全为 0、存在很大的整数、存在负数如果题目允许。以最小路径和为例输入: [[1,3,1], [1,5,1], [4,2,1]] 输出: 7输入: [[5]] 输出: 5输入: [[1,2,3]] 输出: 6这三组用例跑过基本能说明核心逻辑和边界处理都没问题。如果再写一道最大乘积题一定别忘了一个负数组合的用例。把这些习惯培养成条件反射比多刷十道题更值钱。7. 考场避坑与时间分配经验7.1 选择题别恋战卡住就标记跳大厂笔试的选择题通常设置 50 到 60 分钟但后面还有编程题所以选择题部分不能拖。我有一次就是在一道 TCP 状态转换的题上死磕结果编程题只剩十分钟原本会做的题也来不及写非常可惜。经验是选择题平均每题控制在两分钟以内超过三分钟还没思路就先选一个最可能的答案并标记先往下走等编程题写完有余力再回头检查。所有题目在总分里权重是均匀的为了两分丢掉二十分太不划算。7.2 编程题提交前必做的三件事编程题写完之后不要着急点提交。第一件事是重新读一遍题目要求确认输出格式是不是完全匹配包括换行、空格、大小写第二件事是检查变量类型数组下标会不会越界极端输入下会不会溢出第三件事是跑至少一组手算过的测试用例。这三件事是很多同学的失分点我见过太多“思路完全正确但提交 0 分”的案例根源就是没检查格式和边界。7.3 复盘方法把笔试题变成知识树考后复盘比刷题更重要。我自己的习惯是每套笔试题做完先不看答案把所有错题按考点分类画出一棵知识树。比如动态规划这一个分支挂上状态定义、转移方程、初始化、优化方法四个子节点每遇到一道新题就往对应子节点里补充。这样坚持几套题下来你会发现不同题目的考点在快速收敛考来考去其实就那么几十个核心点。百度这套 2016 年的笔试题虽然出版年份早但它的考点分布和现在的主流笔试高度重合用来打底非常合适。最后再说一句没写在文档里的经验笔试不只考察知识储备也考察你在时间压力下的取舍能力。会做的题稳稳拿到分不会做的题果断放弃好过在一道题上反复纠结。把每一次模拟笔试都当成真实战场来练真正上考场时心态会稳很多。