人人网2015研发笔试题解析:数据结构与算法核心考点复盘
刚整理完手头这批老笔试题翻到“人人网2015研发笔试卷A”的时候突然有点感慨。2015年那会儿人人网还在校园社交赛道上挣扎着转型移动端和PC端并行技术栈五花八门笔试的考察范围也特别杂——从基础的数据结构、操作系统到网络、数据库、算法题几乎全覆盖。现在市面上很多公司的笔试题越来越“八股化”反而少了当年那种“什么都考一点、但每道题都值得琢磨”的味道。这篇博文我就拿这份卷子当样本把当年这类研发笔试卷的出题逻辑、每类题型的解题思路、踩过的坑以及这些题目背后真正想考察的能力完整拆一遍。如果你正在准备大厂研发岗笔试或者纯粹想看看五年前的中型互联网公司面试官脑子里在想什么这篇文章都值得往下看。1. 试卷的整体定位与出题思路1.1 2015年研发岗笔试卷的大背景那时候的互联网公司笔试还没有现在这么成熟的在线评测系统很多公司还是发纸质试卷或者邮件发PDF让你在规定时间内做完回传。人人网这份研发笔试卷A从题型结构上看基本遵循了当时互联网公司笔试的“标准配置”选择题含多选简答题编程题。但跟现在很多公司“海量刷题式”的笔试不同2015年那会儿的笔试题更看重基础功和应变能力。一方面是因为当年的招聘量没有现在这么夸张面试官有精力逐题批改另一方面是那时候的技术栈远没有现在这么统一候选人可能用过PHP、Java、C、Python、Node.js中的任何一种所以笔试必须考察语言无关的基础能力。这份卷子的典型特征也很明显选择题里面会混入一些“文字游戏”题题干里刻意设置了容易忽略的条件比如“长度为n的数组查找第k大的数时间复杂度最低是多少”很多人在“排序后取第k个”这个思路上就直接跳进陷阱了。这类题在当年的笔试里非常常见考察的不是你会不会背算法而是你在有时间压力的情况下能不能保持思维缜密。1.2 试卷考察的四大核心能力把这份卷子的所有题目归类之后你能清晰看到出题人想考察的四块能力基础功底数据结构、操作系统、计算机网络这些计算机核心课程的知识掌握程度。这块占比最大大概在40%左右因为这是区分“科班出身”和“半路出家”的硬指标。算法思维不光是能不能写出排序、查找、遍历更看重你对时间复杂度、空间复杂度的敏感度。典型题目就是“给定一个数组找出出现次数超过一半的数字”看似简单但要做到O(n)时间、O(1)空间就需要动点脑子。工程意识这部分体现在答题的规范程度上。比如编程题要求处理边界条件、考虑内存占用、注意输入输出格式。很多人在笔试时能写出核心逻辑但忽略了链表为空、指针为NULL、字符串以\0结尾这些细节在实际批改时扣分很狠。学习潜力有些题目会超纲比如涉及“平衡二叉树旋转过程”“TCP四次挥手的状态变化”这些知识如果在学校里没学过现场很难推出来。出题人想通过这类题判断候选人的知识边界在哪里以及有没有快速学习的能力。提示如果你现在准备笔试看到这里可以先停下来想想——你最近刷题的时候是只刷“会做的”还是也刷“不会做但是看了解析能懂”的后者才是真正提升能力的地方。1.3 从题型分布看人人网的研发需求从试卷的题型分布倒推你能看出来2015年的人人网后端技术团队主要用什么技术栈。选择题里大量出现Linux操作系统的内容比如进程间通信方式、文件系统inode、内存分页管理说明后端服务跑在Linux服务器上而且对系统底层原理要求不低。数据库相关题目偏MySQL考察索引的数据结构、事务隔离级别、SQL优化这跟当时主流互联网公司的技术方案是一致的。编程题虽然不是特别难但涉及字符串处理、链表操作、二叉树遍历说明日常开发中确实需要处理这类基础问题。这给我们一个启发笔试的题目方向往往就是公司业务技术栈的映射。比如电商公司喜欢考高并发场景设计游戏公司喜欢考状态机和A*寻路做社交平台的自然而然会往内容分发、关系链、消息推送这些方向靠。你准备笔试的时候如果能提前调研目标公司的业务方向有针对性地复习效率会高很多。2. 基础题逐题拆解与答题思路2.1 数据结构看似简单实则全是坑数据结构这块人人网这份卷子考得不算偏但每道题都有它的“小九九”。我挑几道典型的详细说说。第一道题是关于链表的“在单链表中删除一个指定节点只给该节点指针不给头指针时间复杂度是多少”答案是O(1)方法是把下一个节点的值复制到当前节点然后删除下一个节点。但前提是这个指定节点不是尾节点。如果这个节点是尾节点那就只能遍历找到前驱节点来删时间复杂度是O(n)。当年这道题很多人直接写了O(n)丢分丢得特别冤枉。这个题考察的知识点是链表的物理结构——链表的删除操作本身是O(1)但找到前驱节点需要遍历。第二道题是栈和队列的转换“用两个栈实现一个队列入队和出队的平均时间复杂度分别是多少”答案是入队O(1)出队摊还O(1)。思路是入队直接压入stack1出队时如果stack2为空把stack1的所有元素弹出来压入stack2再弹出stack2的栈顶。这个题的考点是摊还分析amortized analysis也就是虽然某一次出队操作可能触发一次大规模的元素转移O(n)但每个元素最多被转移一次所以平均下来每个操作还是O(1)。很多人在笔试时直接回答“出队O(n)”虽然也不算全错但没有体现出对摊还分析的理解。第三道题是二叉树的遍历“已知二叉树的前序遍历和中序遍历能否唯一确定这棵二叉树如果能后序遍历是什么”答案是可以唯一确定。前序遍历的第一个节点是根节点在中序遍历中找到根节点的位置左边是左子树右边是右子树。然后递归地处理左右子树。这个题不复杂但考察的是对二叉树遍历本质的理解——前序、中序、后序三种遍历分别是在什么时机访问根节点三者之间的信息冗余关系。类似的变体是“只知道前序和后序能不能唯一确定二叉树”答案是不能因为当某个节点只有左子树或只有右子树时前序和后序无法区分左右。2.2 操作系统进程、线程与内存管理的高频考点操作系统在当年的互联网笔试里占据了相当大的比重因为后端服务本质上就是运行在操作系统上的进程集合。人人网这份卷子涉及了进程间通信、线程同步、内存管理三个核心方向。进程间通信IPC那道题问的是“在Linux下进程间通信的方式有哪些各自的特点和应用场景”。这是一个典型的“背诵理解”题。完整的答案应该包括管道pipe特点是单向、亲缘关系进程间使用命名管道FIFO可用于无亲缘关系的进程间通信消息队列按消息类型读取适合小块数据的可靠传输共享内存速度最快但需要自己处理同步互斥问题信号量主要用于进程间的同步和互斥常与共享内存配合使用套接字socket适用于不同机器上的进程通信信号用于异步事件通知但不能传递大量数据。只回答出四五种其实也够但如果能说出“共享内存是最快的IPC方式因为不需要内核在用户态和内核态之间拷贝数据”这种深入理解面试官在批改时会做标记。线程同步那道题考的是互斥锁和信号量的区别。很多人会把这两个概念混淆实际上它们虽然有相似之处但场景完全不同。互斥锁是“锁”同一个时刻只有一个线程能持有信号量是“计数器”可以允许多个线程同时访问有限数量的资源。举个例子互斥锁像单人厕所一次只能进一个人信号量像停车场有N个车位停满之后后来的车就得等。这个例子我在好几个面试场合给候选人讲过能秒懂的人通常基础都不差。内存管理那道题考的是“页面置换算法”典型的题目是“给定一个访问序列计算在FIFO、LRU、OPT三种页面置换算法下的缺页次数”。这个题其实不难但很多人会栽在边界条件上——比如初始时内存是空的第一次访问某个页面也算一次缺页。还有一个容易忽略的点是页面置换算法发生在“内存满了且要访问的页面不在内存中”时如果内存没满只需要加载页面到内存不需要置换。2.3 计算机网络TCP/IP协议栈是重头戏网络这块人人网作为社交平台对候选人的网络基础要求很高毕竟消息推送、关系链数据同步、图片加载这些核心功能全都依赖网络通信。这份卷子考了TCP三次握手、四次挥手的状态变化以及TCP与UDP的区别。TCP三次握手的过程现在很多技术博客上都写得非常详细了但笔试的时候有个细节容易忽略——状态名要写准确。从客户端视角初始是CLOSED状态发起连接后变成SYN_SENT收到服务器的SYNACK之后变成ESTABLISHED。从服务器视角初始是LISTEN状态收到客户端的SYN之后变成SYN_RCVD发送SYNACK后收到客户端的ACK变成ESTABLISHED。很多人张口就来“三次握手分别是SYN、SYNACK、ACK”但状态转换写不全。笔试不仅仅是考察知识点本身更是考察记忆的精确度。TCP四次挥手的状态变化也是一样主动关闭方要经历FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT被动关闭方要经历CLOSE_WAIT、LAST_ACK。面试官特别喜欢追问一个问题“为什么TIME_WAIT状态要等待2MSL”标准回答是第一保证主动关闭方最后发出的ACK能到达被动关闭方如果ACK丢失被动关闭方会重发FIN主动关闭方需要能够收到第二确保旧连接中的所有报文在网络中消失避免影响新连接。这个知识点直到今天仍然是技术面试的高频考点值得彻底吃透。3. 算法题的完整解答与复杂度优化3.1 题目一数组中出现次数超过一半的数字这是一道非常经典的面试题在《剑指Offer》里也有收录。题目描述是“数组中有一个数字出现的次数超过数组长度的一半请找出这个数字。要求时间复杂度O(n)空间复杂度O(1。”最简单的思路是排序后取中间值时间复杂度O(nlogn但不符合要求。用HashMap统计每个数字出现的次数时间复杂度O(n)但空间复杂度O(n也不符合要求。这道题的正确解法有很多种我这里讲两种比较有代表性的。解法一是“摩尔投票法”。核心思想是如果一个数字出现的次数超过总数的一半那么用一个计数器遇到相同的数字加一遇到不同的数字减一当计数器减到零时更换当前候选数字。遍历完整个数组后当前候选数字就是我们要找的数字。原理是一个超过一半的数字即使在抵消过程中被换掉最终也一定会留在候选位置上。实现起来非常简洁int majorityElement(int* nums, int numsSize) { int candidate nums[0]; int count 1; for (int i 1; i numsSize; i) { if (nums[i] candidate) { count; } else { count--; if (count 0) { candidate nums[i]; count 1; } } } return candidate; }解法二是基于快速排序的partition操作。因为一个元素出现次数超过一半所以数组排序后中间位置的那个元素一定就是答案。我们可以不用完整排序而是利用快排的partition找到中位数。每次partition会把一个元素放到它最终的位置如果该位置在数组中间直接返回如果在中间位置左边则只在右边找如果在右边则只在左边找。平均时间复杂度O(n)但最坏情况下会退化成O(n²不过在随机化选取pivot的情况下几乎不会发生。我当年批改这份卷子的时候发现很多候选人虽然写出了摩尔投票法的代码但忽略了输入检查——如果数组里根本不存在出现次数超过一半的数字代码返回的候选值是不正确的。严谨的做法是遍历完后再扫一遍数组确认候选数字的出现次数确实超过一半。这个细节很重要体现了工程思维中的防御式编程意识。3.2 题目二字符串的全排列“给定一个字符串输出它的所有排列。”这道题考的是递归和回溯的经典结合。核心思路是把字符串看作两部分——第一个字符和剩余字符。把第一个字符依次跟后面每一个字符交换然后递归地对剩余字符做全排列。递归的终止条件是当前处理到了字符串的最后一个字符此时输出当前排列。def permute(s, l, r): if l r: print(.join(s)) else: for i in range(l, r 1): s[l], s[i] s[i], s[l] # 交换 permute(s, l 1, r) # 递归 s[l], s[i] s[i], s[l] # 回溯这个题写代码不难难的是考虑两个细节。第一个是重复字符问题如果字符串中存在重复字符上述代码会输出重复的排列。解决方案是在交换之前判断如果当前要交换的字符在之前已经出现过就跳过这次交换。第二个是非递归解法可以用字典序算法从当前排列出发找到下一个字典序更大的排列不断迭代直到返回字典序最小的排列。这个算法在STL的next_permutation中有实现笔试时如果被问“能不能不用递归实现”才算真正挖到了深度。3.3 题目三求最大连续子数组和题目描述是“给定一个整数数组找到一个具有最大和的连续子数组至少包含一个元素返回其最大和。”经典解法是Kadane算法核心思想非常简单遍历数组维护两个变量——current_max表示以当前元素结尾的最大子数组和global_max表示到目前为止的全局最大子数组和。每遍历一个元素current_max max(nums[i], current_max nums[i])然后更新global_max max(global_max, current_max)。int maxSubArray(int* nums, int numsSize) { int current_max nums[0]; int global_max nums[0]; for (int i 1; i numsSize; i) { current_max (nums[i] current_max nums[i]) ? nums[i] : current_max nums[i]; global_max (global_max current_max) ? global_max : current_max; } return global_max; }这个算法的精髓在于理解“为什么要用current_max nums[i]而不是从头开始”因为子数组是连续的要么往前扩展接在前一个元素结尾的子数组后面要么从当前位置重新开始。这个“接续或重开”的决策是很多动态规划题目的共同模式。如果面试官继续追问“如果数组是环形数组呢”解法是考虑两种情况——最大子数组不跨越边界或者跨越边界此时等价于“数组总和减去最小子数组和”取两者较大值。3.4 编程题作答时的时间与空间复杂度意识笔试编程题跟平时写业务代码有一个很大的区别平时你写完功能能跑就行但笔试时必须分析时间复杂度和空间复杂度。这不是额外要求而是核心要求。我在批改试题时会看候选人有没有在代码旁边写上复杂度说明如果写了说明他对自己的代码有清晰的认识如果没写大概率是没想清楚。另外一个容易被忽视的点是笔试环境往往没有IDE的自动补全和语法提示手写代码的规范程度直接反映了你的代码功底。变量命名是否有意义缩进是否统一是否处理了空指针、空数组的边界情况这些细节在笔试中都是阅卷人的“第一印象分”。4. 常被忽略的细节考点与实战避坑4.1 计算机基础“边角料”题型的复习策略人人网这份卷子里面有一类题是专门考“边角料”的比如int类型占多少字节、sizeof和strlen的区别、指针和引用的区别、static关键字的多重作用、数组名和指针的异同。这些考点单拎出来都不难但在综合笔试卷里它们就像地雷一样专门炸那些“基础不牢靠”的候选人。以static关键字为例它在C语言中有三个作用修饰局部变量延长生命周期到程序结束但作用域不变、修饰全局变量或函数限制作用域在当前文件内、在C中修饰类的成员变量所有对象共享一份拷贝和成员函数只能访问静态成员。很多人能答出前两个第三个经常忘记这就是阅卷人区分“熟练”和“背过”的关键点。复习这类考点的策略我建议不要只背“答案是A”而要把每个选项为什么错搞清楚。比如一道选择题问“下列哪个不是进程间通信方式”选项里有“回调函数”答案是回调函数。但如果你只是记住了答案下次换个问法——“事件驱动模型中线程间同步用的是哪种机制”——你可能又懵了。把知识点织成网而不是背成散点是应对“边角料”题的唯一有效方法。4.2 数据库题目索引与事务隔离级别的综合应用数据库是研发笔试里的常客人人网这份卷子也不例外。索引相关考点集中在B树索引的底层结构、聚簇索引与非聚簇索引的区别、联合索引的最左前缀原则、什么时候索引会失效。事务相关考点集中在ACID四特性、隔离级别的定义与问题脏读、不可重复读、幻读。一道典型的综合题是“一张用户表有(user_id, age, city)三列有一条SQLSELECT * FROM user WHERE city Beijing AND age 20请为它设计最优索引。”很多人不假思索就写CREATE INDEX idx_city_age ON user(city, age)看起来没毛病。但仔细想想如果这个表的数据量很大而且查询非常频繁还有没有更好的方案如果90%的用户都在北京那么在city字段上建索引其实没有太大选择性收益这时候可能应该考虑在age上建索引或者用覆盖索引把查询字段都包含进去。这种题没有标准答案关键在于你要展示出自己的思考过程而不是直接给结论。4.3 手写代码时的常见“低级错误”避坑清单我在批改过程中发现候选人在手写代码时经常犯一些“低级错误”这些错误跟算法能力无关纯粹是粗心或者习惯不好。整理成避坑清单供大家自查忘记返回值特别是在递归函数里边界条件分支返回了但主逻辑分支忘了返回。编译器虽然会警告但笔试是手写代码没有编译器帮你检查。数组越界访问arr[n]而不是arr[n-1]或者循环边界写成i n导致多跑一次。笔试时强烈推荐在代码旁边画一个简单的示例数组下标从0开始标好对照着写循环。空指针/空数组处理很多人的算法逻辑默认输入非空但笔试测试用例里一定会包含空数组、空字符串、只有一个元素这些极端情况。整数溢出当处理数值较大的运算时比如计算数组元素之和、求中位数时的left right要想到用long long或者left (right - left) / 2来避免溢出。注意手写代码的规范性不只是为了过笔试。我做了这些年技术面试可以负责任地说——一个在笔试中代码规范的人在实际工作中写出的代码也不会差到哪里去。代码习惯是长期养成的临时抱佛脚很难改。4.4 时间分配一份笔试卷的130分钟怎么花人人网这份笔试卷A的时长是150分钟满分100分。我根据题型分布给一个比较合理的时间分配建议选择题含多选建议控制在40分钟内简答题控制在40分钟内编程题控制在60分钟内剩下10分钟用于检查。选择题中遇到不会的题先标记跳过不要死磕。一道选择题如果超过3分钟还没思路说明这个知识点你的掌握程度还达不到考场临时思考就能解出来的水平不如节省时间去做后面的简答题和编程题。简答题建议先列大纲再作答因为阅卷通常是采点给分你写一堆没条理的文字阅卷人很难找到你的得分点。编程题建议先写注释后写代码把思路用注释表达出来一方面自己能理清逻辑另一方面阅卷人也能看到你的思考路径。5. 如何高效复盘一份试卷的价值5.1 复盘不是看答案而是重构解题路径拿到一份参考答案之后很多人的复盘方式是“哦原来这道题这么做我记住了”。这种复盘效率极低。有效的复盘应该是先不看答案重新做一遍错题然后追问自己三个问题我刚才为什么没想到这个解法这个解法的核心洞察是什么如果题目条件变化一个字比如数组有序变无序、单链表变双向链表解法会怎么变就拿“两个栈实现队列”这道题来说如果你只是在错题本上写“入队压入stack1出队先搬stack2再弹出”那下次面试官问“两个队列实现栈”你大概率还是不会。但如果你复盘时想清楚了一个核心洞察——栈后进先出队列先进先出用两个栈的目的是把数据的顺序翻转两次翻转两次之后顺序就跟原来一致了——那么“两个队列实现栈”你也能立刻推出来入栈直接入队到queue1出栈时把queue1里的元素除了最后一个全部搬到queue2然后弹出剩下的那个最后交换queue1和queue2的引用。5.2 通过笔试反推知识盲区建立补全地图一套笔试卷最大的价值是帮你画出一张“知识盲区地图”。我当年按下面这种方式处理错题把每道错题对应到知识点比如“错了一道关于LRU淘汰算法的题”对应的是“操作系统内存管理/缓存策略”。然后把这个知识点所在的整个章节过一遍而不是只背这一道题。LRU只是页面置换算法中的一种跟它并列的还有FIFO、OPT、Clock算法它们的命中率、实现难度、适用场景各不相同。找到这些算法的共同思想——在有限资源下做决策核心是“未来不可知只能基于历史和启发式信息做最优选择”。理解了这个思想不管题目怎么变你都有解题框架。建立盲区地图还有一个附带的好处你会发现不同科目的知识是相互关联的。比如LRU缓存策略在操作系统的页面置换里会出现在Redis的内存淘汰策略里也会出现在系统设计面试的缓存设计中还会出现。一旦把这些关联打通你的知识体系就从一个一个孤立的点连成了网状结构。5.3 把笔试题当成系统设计面试的跳板很多人做完笔试题就扔一边了觉得笔试和后面的技术面试是两码事。其实不完全对。笔试里的很多题目在技术面试中会被放大成系统设计问题。举个例子笔试考了“两个栈实现队列”面试官在系统设计里可能会问“如何设计一个支持消息先进先出且能处理突发流量的消息队列”——虽然复杂度完全不是一个量级但底层逻辑是对齐的你需要一个临时存储你需要控制数据流的顺序你需要应对消费者处理速度和生产速度不匹配的问题。同样笔试题“求数组中出现次数超过一半的数字”看起来只是一个数学技巧。但换个角度想这背后的分布式投票思想多数派决策在很多一致性算法里都有体现比如Paxos和Raft中的多数派投票机制。把笔试题还原到它来源的计算机科学基础你就能从一个“会做题的人”变成“懂原理的人”这也是技术面试中加分的关键。6. 人人网研发笔试卷A的横向对比与启示6.1 与其他互联网公司笔试卷的异同把人人网2015研发笔试卷A跟同年其他互联网公司的笔试卷横向对比你会发现一些有趣的规律。跟BAT相比人人网的试题整体难度偏低算法题没有出现特别复杂的动态规划和图论题目更多集中在基础数据结构操作。但跟创业型公司相比人人网又明显更看重计算机基础理论的扎实程度操作系统和网络的题目占比不低。这个反差其实反映了当时人人网的尴尬处境作为一家有一定体量的上市公司它希望招到“科班出身、基础扎实”的工程师来维护现有的复杂系统但跟一线大厂相比它在吸引力上又不占优势所以笔试难度必须控制在“让大部分候选人能做出一半以上”的水平否则根本约不到面试。理解这个背景你就能明白为什么这份卷子的题目编排是“基础题为主、难题少量、编程题不出格”。6.2 从2015年到现在的笔试演变趋势对比今天各大厂的笔试题最大的变化不是题型而是考察的深度和广度都翻了好几倍。2015年的笔试题选择题还有“以下哪个排序算法是稳定的”这种直给型题目现在很多公司的笔试直接把题目挂在在线评测系统上全是编程题还带测试用例判定。换句话说现在的笔试更像“比赛”而不是“考试”。为什么会发生这种变化一方面是候选人数量激增纯主观题的人工批改成本太高客观的在线评测系统能大规模筛人另一方面是技术栈的标准化程度提高了Java、Go、Python三分天下笔试题目可以基于主流语言设计得很“工程化”。但有一个趋势是没变的不管题目形式怎么变数据结构、算法、操作系统、网络这些计算机基础的核心内容依然是考察的重中之重。这就像一个建筑的地基无论上面的楼层盖成什么样地基本质上没有变化。6.3 做这套旧笔试的真正价值一套五年前的笔试卷在今天还有多少参考价值我的答案是题目本身的价值有限但它背后的思维方式、考点选择、答题策略具有很强的参考性。你可以用它来检验自己的计算机基础是否体系化也可以用它来做模拟练习看看自己在没有IDE和搜索引擎的情况下能不能按时完成。更重要的是这套卷子提醒我们技术面试的底层逻辑始终是“考察候选人解决问题的能力”而不是“考察候选人记住了多少知识点”。2015年的笔试题是让候选人手写代码解决具体问题今天的算法题也是让候选人写代码解决具体问题形式变了内核没变。如果你能通过做这套旧试卷体会到“分析问题—拆解问题—设计解法—编码实现—测试验证”这一整套思维链条那它带给你的价值比刷十套新鲜题还要大。我个人在这几年带新人的过程中发现凡是能静下心来做一套旧笔试并认真复盘的人在项目中也通常能展现出更好的问题分析能力和代码规范意识。这大概就是好题目和认真做题的人之间的相互成就。
