ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

人人网2015研发笔试题D卷复盘:算法基础与系统设计

人人网2015研发笔试题D卷复盘:算法基础与系统设计 2015年那会儿人人网的研发笔试卷在社交类公司里算是有代表性的量大、面广还有一道压轴的系统设计题。我记得自己当时拿到D卷第一感觉是“基础题不难但想拿满分不容易”编程题看起来眼熟真正下笔却发现边界条件一堆坑最后的系统设计题更是直接把人从“写代码”拉到了“做架构”的层面。这篇文章不是官方题解是我基于亲身经历和同行交流还原出的一份D卷复盘把每类题背后的考点、解题思路和失分原因都拆开讲清楚希望对正在准备社交、内容平台类公司研发岗笔试的同学有参考价值。1. 这场笔试为什么值得复盘先交代一下背景。人人网在2015年仍然是以PC端社交为主、移动端快速转型的阶段研发岗位要面对的是千万级注册用户、高并发的Feed流、私信消息系统、好友关系链这些典型社交业务场景。笔试考察的范围因此非常强调三件事计算机基础是否扎实、算法思路是否清晰、以及对大规模分布式系统有没有基本认知。D卷的完整结构大致如下题型数量分值占比考察方向选择题20题约30%数据结构、操作系统、网络、数据库填空题6题约15%概念细节、复杂度、协议状态编程题2题约30%字符串处理、海量数据TopK系统设计题1题约15%Feed流、缓存、消息队列开放性论述1题约10%对社交产品的技术理解整体时间是120分钟。说实话这个时间挺紧张的选择题如果犹豫后面编程题根本写不完。我自己当时的前40分钟基本都在做基础题遇到拿不准的先标记跳过保证编程题有完整的时间。很多人以为笔试只要刷题就够了但人人网这套卷子给我最大的感受是它考的不仅是“会不会”更是“熟不熟”。比如选择题里有一道“TCP三次握手的第三次握手失败了会怎样”这种题目不背八股文容易懵背了八股文但不理解状态机照样会错。再比如填空题问到“一个进程最多能有多少个线程”如果不结合栈空间和虚拟内存来算很容易掉进坑里。所以这篇复盘我会一条一条讲清楚每道题想考什么、标准解法是什么、容易错在哪里以及从研发岗位的实际工作看这些知识点到底用在什么地方。对于现在准备校招笔试的同学这套题的参考价值在于——它非常贴近国内互联网公司研发岗的真实考察风格比单纯刷LeetCode更接近“工程 算法 基础”的综合要求。2. 试卷结构一览与典型失分点2.1 选择题的隐藏陷阱D卷选择题覆盖了数据结构、操作系统、计算机网络、数据库四块。表面上都是常规考点但人人网出题喜欢在“似懂非懂”的概念上做文章。举几个例子“在TCP连接中主动关闭连接的一方进入TIME_WAIT状态等待时间大约是多少”这个题本身不难答案是2MSL但很多人会忘记TIME_WAIT出现在主动关闭方而不是被动关闭方。实际业务中如果服务器端大量主动关闭连接会出现大量TIME_WAIT连接导致端口被占用。这题考的就是“状态机”和“工程现象”的结合。“以下哪种数据结构最适合实现LRU缓存”选择有数组、链表、HashMap、双向链表加HashMap。很多人选HashMap但LRU需要同时满足O(1)查找和O(1)淘汰必须双向链表保存访问顺序HashMap保存key到节点的映射。这道题是经典的“看似简单实则考组合结构”。“InnoDB索引为什么用B树而不是B树”如果只背了“B树矮胖”得不了满分还要答出“叶子节点有链表适合范围查询非叶子节点不存数据磁盘页能存更多key减少IO次数”。这些选择题的共性是它们背后都对应着真实工程问题。LRU缓存对应Redis内存淘汰B树对应MySQL千万级数据查询TIME_WAIT对应高并发短连接服务。如果只是埋头刷题而从不追问“这个知识点到底在哪用”这类题目很容易被账面知识迷惑。2.2 填空题最爱考细节填空题是整套卷子中“背诵成本”最高的部分。D卷里有一道印象很深“进程间通信方式有哪几种请列举至少四种。”答案是管道、消息队列、共享内存、信号量、套接字、信号。这个题本身不难但很多人会漏掉“信号量”因为信号量一般被归为同步机制而它确实也是IPC的一种。还有一道“使用Linux epoll模型时事件驱动机制是____。”答案是IO多路复用或者更具体地说epoll是Linux下IO多路复用的一种实现基于事件通知机制避免了select和poll的轮询开销。这题考察的就是服务端高并发编程的基础模型。人人网的IM和Feed流服务都属于IO密集型应用epoll几乎是后端开发绕不开的知识点。填空题的失分点集中在“知道但不精确”。比如填“死锁产生的四个必要条件”时写了“互斥、请求保持、不可剥夺、循环等待”顺序反了也会被扣分因为阅卷标准里对术语顺序有要求。这类分丢得最冤解决办法只有一条在理解的基础上准确记忆不要只记大意。2.3 整场考试最大的时间杀手我到现在还记得考试时最耗时间的不是编程题而是一道多选题“以下关于HTTP协议的说法正确的有哪些”。选项涉及GET和POST的区别、Cookie和Session的关系、Keep-Alive的作用、HTTPS的加密过程。这个题难在每一个选项单独看都是对的但它故意混入了一个错误表述“HTTPS就是HTTP加SSL所以只需要在传输层做对称加密。”这个说法部分正确但不完整HTTPS握手过程还要使用非对称加密交换会话密钥实际通信用对称加密。如果不熟悉TLS握手流程很容易误选。这种多选题非常考验知识体系的完整度。我的建议是做题时碰到这种选项不要靠“感觉对”来选而是逐个用自己的话解释一遍原理。解释不了的选项基本就是知识漏洞。3. 编程题拆解从暴力解到最优解3.1 字符串循环移位包含判断D卷第一道编程题是这样的“给定两个字符串s1和s2判断s2是否能由s1循环移位得到。例如s1 AABCDs2 CDAA则返回Trues1 ABCDs2 ACBD则返回False。要求写出代码并说明复杂度。”这道题是典型的前程无忧/牛客网上经常出现的字符串题。最容易想到的解法是穷举所有移位结果逐个比较。boolean isRotated(String s1, String s2) { if (s1 null || s2 null || s1.length() ! s2.length()) { return false; } int n s1.length(); for (int i 0; i n; i) { String rotated s1.substring(i) s1.substring(0, i); if (rotated.equals(s2)) { return true; } } return false; }这个解法思路直观时间复杂度O(n^2)substring加equals空间复杂度O(n)。笔试时这样写只能拿基本分想拿高分还得再进一步。最优解的核心是有一个经典的数学结论s2如果是s1循环移位得到的子串那么s2一定包含在s1 s1中。这句话怎么理解循环移位本质上就是把原字符串的头接到尾上如果将s1复制一份拼在自己后面所有循环移位结果都会出现在这个大串中且长度与s1相同。所以代码可以写成boolean isRotated(String s1, String s2) { if (s1 null || s2 null || s1.length() ! s2.length()) { return false; } String combined s1 s1; return combined.contains(s2); }这样时间复杂度取决于contains的实现。在Java中String.contains底层使用indexOf朴素实现是O(n*m)的实际库函数在多数情况下有优化但最坏复杂度仍不理想。面试时如果继续追问应该再接上KMP算法把复杂度降到O(n)int kmpSearch(String text, String pattern) { // 构建next数组 int[] next getNext(pattern); int i 0, j 0; while (i text.length() j pattern.length()) { if (j -1 || text.charAt(i) pattern.charAt(j)) { i; j; } else { j next[j]; } } if (j pattern.length()) { return i - j; } return -1; }这道题想要考察的其实是“能不能把一个枚举问题转化为字符串匹配问题”。很多人在笔试时会忘记判断s1和s2的长度是否相等甚至没有考虑空字符串和null这都是边界条件的失分点。我在考试时先用暴力解法写上再在注释里补充了最优解思路后来反思这样其实不如直接写最优解因为阅卷时间有限改卷人通常只看主方法逻辑。3.2 海量日志中统计访问频率最高的Top 100 IP第二道编程题是“某网站一天产生海量访问日志每行记录包含访问IP内存只有1GB请统计访问次数最多的前100个IP并说明方案。”这题是经典的“海量数据TopK”问题。上来不要急着写堆排序先想清楚两个核心问题数据量有多大如果日志达到几百GB直接全部读入内存不现实。TopK需要什么数据结构维护一个大小为100的最小堆堆顶是当前最小当新元素比堆顶大时替换堆顶最后堆里就是频率最高的100个。完整方案分两步第一步将大文件切分为多个小文件采用哈希取模的方式保证同一个IP总是进入同一个文件。比如对IP字符串取哈希值再对文件数取模hash(ip) % N每个小文件的大小控制在内存可承受范围内。第二步对每个小文件建立HashMap统计每个IP的出现次数再用容量为100的最小堆维护Top100。伪代码如下// 第一轮hash分桶 String ip readLine(); int fileIndex Math.abs(ip.hashCode()) % N; writeToFile(fileIndex, ip); // 第二轮每桶内统计并维护TopK MapString, Integer countMap new HashMap(); String ip readLine(); countMap.put(ip, countMap.getOrDefault(ip, 0) 1); PriorityQueueMap.EntryString, Integer minHeap new PriorityQueue(100, Comparator.comparingInt(Map.Entry::getValue)); for (Map.EntryString, Integer entry : countMap.entrySet()) { if (minHeap.size() 100) { minHeap.offer(entry); } else if (entry.getValue() minHeap.peek().getValue()) { minHeap.poll(); minHeap.offer(entry); } }这里有个容易出错的地方哈希取模会导致某个IP被分到多个文件吗不会相同的IP计算出的哈希值相同对N取模结果也相同所以同一个IP只会出现在同一个文件中。这保证了每个桶内统计结果的完整性。还有一个高频追问“如果内存小到连HashMap都放不下怎么办”答案是继续对单桶进行二次哈希拆分或者改用排序法先对每个桶内的ip排序排序后相同的ip相邻一次遍历即可统计频次。这个思路在海量数据面试题中通用笔试时可以在方案描述里提一句作为加分项。3.3 编程题通用避坑清单两道编程题做完我给自己总结了一套避坑清单后面做其他公司笔试试卷时也一直在用先确认输入边界null、空字符串、0、负数、超大数都要问清或默认处理。字符串题先想“能否转化为已有成熟算法”比如包含问题转成KMP、回文问题转成Manacher。TopK问题先问数据量和内存限制再决定是堆排序、快排改版还是分桶。代码写完后至少手动跑一个正常用例和一个边界用例。复杂度分析要写在注释中这能体现“计算意识”阅卷人非常看重。4. 基础题考点操作系统、网络与数据库4.1 操作系统互斥、死锁和内存分配D卷操作系统部分的题目数量不算多但覆盖面很广。大致包括进程与线程区别、死锁必要条件、银行家算法、虚拟内存、页面置换算法。有一道题目是“两个线程并发执行对一个共享变量进行i操作最终结果一定比预期大吗”答案是错误甚至可能比预期小。因为i不是原子操作它包含读取、加一、写回三步两个线程同时读取同一个旧值各自加一后写回最终只加了一次这就是“丢失更新”。对应到实践这就是为什么多线程场景要用AtomicInteger或者加锁。另一道很容易错的题目是“虚拟内存的作用是什么”很多人答“扩大物理内存”严格来说不对。虚拟内存的实质是让每个进程拥有独立的虚拟地址空间通过页表映射到物理内存再配合缺页中断和页面置换让程序可以使用比物理内存更大的地址空间。它解决的是“内存隔离”和“内存管理”问题而不是真的“扩大”了物理内存。如果现场还考了银行家算法的过程描述不要慌这种题的解题套路是先检查当前剩余可用资源能否满足某个进程的最大需求找到一个可完成的进程回收它的资源再继续找下一个直到所有进程都完成。只要按这个顺序推基本不会错。4.2 网络三次握手、四次挥手和HTTP状态码网络部分几乎是笔试试卷里的固定内容。D卷选择填空里出现了TCP三次握手、TIME_WAIT、HTTP状态码、DNS解析过程。一个高频辨析点是“TCP三次握手为什么不是两次”如果只看理论书答案是为了防止“已失效的连接请求报文段”突然又到达服务端造成资源浪费。更通俗的解释是如果只有两次握手服务端无法确认自己对客户端的发送能力是否正常也无法确认客户端是否已经收到自己的同步包。第三次握手本质上是客户端对服务端同步包的确认没有它服务端会一直处于半开连接状态。TIME_WAIT是另一个高频考法。主动关闭连接的一方发送最后一个ACK后会进入TIME_WAIT持续2MSL。这个状态有两个作用保证最后一个ACK能够到达对方万一丢了可以重传。让本次连接中的所有报文段在网络中消失避免影响新的连接。面试追问往往会落在实践上“高并发短连接下为什么会出现大量TIME_WAIT怎么解决”答案方向是开启keep-alive长连接、调整tcp_tw_reuse参数但要注意网络环境约束、或者让服务端不要主动关闭连接。这个问题的价值在于它把TCP状态机从课本搬到了服务器调优现场。HTTP部分人人网那套题比较喜欢考状态码语义。比如301是永久重定向302是临时重定向304表示资源未修改可直接使用本地缓存503是服务不可用。我当年在304上犹豫过因为平时调试接口时见到304总以为是不允许访问实际上它和缓存强相关。后来在写Feed流服务时静态资源和接口响应都要配合ETag、Last-Modified来返回304减少带宽消耗这才彻底理解它的工程含义。4.3 数据库索引、事务与隔离级别数据库题目的核心是索引和事务。人人网业务中用户关系、Feed消息、私信记录都存储在MySQL这类关系型数据库里索引设计直接影响查询性能。D卷有一道经典的索引题“MySQL InnoDB中主键索引和普通索引有什么区别”主键索引是聚簇索引叶子节点直接保存整行数据普通索引的叶子节点保存的是主键值。因此通过普通索引查询数据时会先查到主键值再回表到主键索引查完整数据这就是“回表”。如果查询列都包含在普通索引中就不需要回表可以做到“覆盖索引”优化。事务隔离级别也是选择题常客。四个级别从上到下依次是读未提交、读已提交、可重复读、串行化。InnoDB默认是可重复读这个默认值和其他数据库不太一样。很多人会忽略“可重复读”在InnoDB里通过MVCC实现快照读不会加锁但当前读select for update、update、delete会加行锁。如果笔试里给了两个并发事务的执行序列问最终结果是什么一定要先判断语句是快照读还是当前读。我在复习事务时用过一个笨办法把四个隔离级别和它们的“脏读、不可重复读、幻读”对应关系画成一张表考前默写直到滚瓜烂熟。这个方法效率很高因为笔试只考结论不考推导。隔离级别脏读不可重复读幻读实现方式读未提交可能可能可能直接读最新版本读已提交不会可能可能每次读生成快照可重复读不会不会可能InnoDB间隙锁可避免事务开始生成快照串行化不会不会不会加锁串行执行4.4 基础题怎么复习才不丢分基础题没有捷径但也不需要题海。我后来带新人时经常说一句话把每个知识点当成“出题人”来复习。复习到TCP握手时问问自己如果我是面试官我会在哪个地方埋坑我会不会问“为什么不是两次”我会不会把TIME_WAIT和服务器调优结合用这种方式复习一遍相当于自己给自己出了一套模拟卷比单纯看书高效得多。操作系统和数据库这两块强烈建议配合线上问题排查经验来理解。如果你没有实战经验就多看看技术博客里的线上故障复盘比如“一条SQL导致CPU飙升”“大量CLOSE_WAIT连接堆积”这些案例看多了基础题里的工程题基本能靠常识答对。5. 系统设计题Feed流后端设计5.1 题目描述和考察意图D卷最后一道大题是一道系统设计题原题大意是“设计一个类人人网的社交Feed流系统用户可以发布动态、查看好友动态列表。要求说明数据存储方案、缓存策略、接口设计并画出请求链路。”虽然不允许画架构图但其实文字描述足够说明方案就可以。这道题考察的不是能不能写出代码而是有没有做过真实系统设计。一个合格的Feed流系统至少要涉及以下模块发布动态服务写入内容触发好友通知。好友关系服务判断用户之间是否好友拉取好友列表。Feed生成服务从关注/好友列表聚合动态。缓存层缓存热Feed和 Timeline。存储层关系型数据库存永久数据Redis存热点数据。我当时把方案分成了“推模式”和“拉模式”两部分来写。5.2 推模式与拉模式的选择推模式也叫写扩散。用户发布一条动态后系统立即把这条动态写入所有好友的Feed收件箱。这种方式读取时只需取当前用户收件箱速度很快但发布者如果好友很多比如百万粉丝一次写入量巨大就是“写放大”。拉模式也叫读扩散。用户发布动态时只写自己的发件箱好友来查看时再去聚合所有好友的动态。这种方式写入成本低但读延迟高尤其当用户关注了几百上千人时需要实时合并大量列表就是“读放大”。实际系统几乎不会只用一种模式而是采用混合方案普通用户之间用推模式好友数有限写扩散成本可控。大V用户为了防止一次发布给百万关注者写入百万条数据改为拉模式只有普通粉丝来读取时才去临时拉取大V的动态。再为每个用户的Timeline加一个Redis缓存缓存未命中时才查DB。用表格描述更清晰方案写入成本读取成本适用场景代表案例推模式高低好友数少、互动频繁微信朋友圈早期模型拉模式低高关注数多、读多写少微博早期拉取方案混合模式中中普通用户推、大V拉大多数社交平台这里要说一个很容易被考生忽略的点笔试阅卷人看设计题时最在意的是有没有“取舍意识”。方案没有绝对最优只有针对场景的最适合。如果你只写了一版推模式也没有任何对比分析得分通常只在中档。如果你先写出两种模式分析各自瓶颈再给出混合方案就算细节不完整分数也会明显更高。5.3 缓存设计如何应对热点动态Feed流还有一个关键问题是缓存。没有缓存用户的每次下拉刷新都要聚合几百上千条好友动态数据库压力可想而知。常规设计是Timeline缓存以用户ID为key存储其Feed流前N条动态ID列表缓存过期时间通常设为10分钟到30分钟。动态详情缓存以动态ID为key存储动态内容用于批量查询详情。热点动态单独缓存对短时间内阅读量暴涨的动态比如被转发过万的内容在缓存里单独设置更长的过期时间并加多级缓存。题目里如果有追问“缓存与数据库数据不一致怎么办”核心回答思路是接受短暂的不一致通过设置过期时间解决写操作优先更新数据库再删除缓存如果删除失败加一个MQ异步重试。这里不需要长篇大论点到CAP和最终一致性就够。还有一种追问方向是“Feed流分页怎么做”。千万不能用MySQL的OFFSET做大偏移分页因为数据库会扫描大量无用行。常见方案是用动态ID做游标分页每次返回最后一个动态ID下一页查询时加上WHERE feed_id last_id ORDER BY feed_id DESC LIMIT 20。这么做既快又稳也是我在面试中最常写的答案。5.4 设计题如何组织答案设计题写在白纸上和写在电脑上不一样阅卷人只能看到最终结果看不到你的思考过程。所以答案组织要有明显层次先用一句话定义场景和限制这是一款面向千万用户、日活百万、Feed内容量巨大的社交应用。列出核心功能发布动态、好友关系、读取Feed列表、点赞评论可选。估算数据量级每日新增动态总量、每个用户好友数、QPS峰值不要求精确但要有数量级概念。设计存储结构分库分表策略、Redis key设计。描述读写链路从客户端请求到服务端响应经过哪些组件。指出瓶颈和优化方向热点问题、缓存穿透、雪崩应对。我当年这道题写得中规中矩没有画图只用了三段式文字功能拆解、存储方案、推拉模式对比。后来跟一个参与过阅卷的学长聊他说设计题得分高的卷子通常都有一个共同点——不堆名词每条方案后面都跟着一句话解释“为什么这么设计”。比如写了“用Redis做缓存”接着就写“因为Feed流读多写少命中率是性能关键”。这样的答案说明考生真正理解设计意图而不是在默写架构词汇。5.5 开放性试题说说你对社交产品后端架构的理解D卷还有一道开放题“如果让你重新设计人人网的后端你会保留哪些模块、改造哪些模块为什么”这类题目没有标准答案考察的是知识迁移能力。我的回答思路是从“单体应用拆分为微服务”切入把用户服务、关系链服务、Feed服务、消息服务、内容审核服务拆开独立部署数据库按业务拆分Feed表、用户表、关系表分开引入消息队列削峰动态发布后写入MQ异步推送给在线用户增加监控和链路追踪系统应对故障排查。现在回看这个答案很多细节已经过时比如那个年代流行是“SOA/微服务”现在可能直接上云原生了。但笔试通常不考技术名词的前瞻性而是考你有没有“拆分意识”“异步意识”“可观测意识”。即使放在今天这三个意识依然是后端架构设计的核心。6. 复盘后的三个教训与备考思路6.1 笔试不是懂就行而是快才不亏整场考试结束后我最大的挫败感不是题目不会做而是“明明都见过却没时间写完”。编程题第一题我写了暴力解才想到最优解第二题只写了一个getNext的框架后面的输入输出和边界代码都没写完。这种状态非常吃亏。后来我给自己定了一条铁律笔试现场第一眼看到题目如果30秒内没有思路先跳过去做后面能拿分的基础题。排序题、选择题、填空题都是确定性得分编程题如果卡住至少要写上暴力解和复杂度分析因为阅卷人通常按步骤给分。为了提升熟练度我在笔试前一个月每天固定做两件事早上15分钟刷四道“高频基础题”——反转链表、判断回文、括号匹配、二分查找变体晚上做一套模拟笔试试卷严格限时。这个习惯在后期非常有效因为基础题的代码几乎变成手指记忆遇到类似题时可以省下大量思考时间。6.2 基础题不用贪多但要建立“概念网络”我见过很多同学备考时拼命刷LeetCode 300题结果操作系统和网络一点没看最后笔试卷基础题错一半。这种刷题策略在人人网这套卷子上非常吃亏因为基础题占比接近一半而且都是“会就会、不会就不会”的确定性题目。基础题的高效复习方式不是背题而是把互相有关联的概念串成网。比如复习“进程和线程”顺藤摸瓜延伸到“锁、死锁、内存可见性、线程池”复习“TCP”自然延伸到“HTTP、HTTPS、DNS、负载均衡”。每从一个知识点出发能延伸出多少个关联考点基本就是你知识体系的覆盖面。这张网越密笔试里遇到陌生题时越有底气因为你可以靠推理而不是背诵来作答。6.3 系统设计题要多写不要只看系统设计题对没有项目经验的学生来说最大的问题是“眼高手低”。看别人的架构方案觉得很简单轮到自己写连接口定义都写不完整。破解办法是找几个经典题目逐个手写设计方案字数控制在500字以内讲清楚场景、存储、缓存、链路、瓶颈就够了。写完之后找人帮你挑毛病或者对照网上优质答案找差距。我当时练习的题目有设计短链接系统、设计关注关系、设计微博Feed流、设计消息已读回执。这些题目虽然场景不同但核心方法论一致拆功能、定存储、选缓存、画链路、做容错。练过三四个之后你会发现80%的设计题都能用自己的框架去套剩下的20%是场景特有的细节比如短链接需要解决哈希冲突消息系统需要解决可靠性投递。另一个很实用的习惯是把“一句话解释为什么”当成设计题的答题铁律。我后来面试其他公司时面试官夸过我“每个方案都会附带理由”这个习惯就是从那场笔试复盘里养成的。它背后的逻辑很简单系统设计本质是权衡没有理由的方案等于没有经过思考。说到这里回到标题里的“D卷”其实它不过是一套笔试题目。但对我来说整理这套试卷的收获远远超过考试本身它让我第一次意识到“工程思维”和“刷题思维”是两码事算法题要讲复杂度基础题要讲精确性设计题要讲取舍。准备笔试的过程本质上就是提前适应一个研发工程师的思考方式。如果你正在备考拿这套题做一次模拟按120分钟严格计时做完后重读一遍自己的答案你会发现最值钱的信息不是“这道题答案是什么”而是“我在时间压力下会在哪里卡住”。把卡住的地方一个个补起来这一趟备考就值了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进