刚看到“完美世界2016研发工程师笔试题”这个标题,估计很多人的第一反应是:都这么多年了,这套题还有参考价值吗?我的回答是:有,而且价值集中在两个维度。一是真题本身的考点分布,可以当成一面镜子,照出当年游戏/互联网公司在筛人时的核心关注点;二是这套题的解题思路,放到今天依然是很多研发岗笔试的底子。那一年移动游戏正在全面爆发,端游IP转手游进入高峰期,完美世界作为老牌端游厂商,笔试命题兼具“游戏公司底色”和“通用研发岗通识”,很适合拿来拆解。
1. 先看这套题的“盘面”:一场笔试暴露了什么样的筛选逻辑
1.1 题量与时间:笔试考察的不只是会不会,更是会不会取舍
当时完美世界研发工程师的笔试通常是线下统一机考或纸笔作答,时间一般在90到120分钟,题量看着不大,但做起来非常挤。整套卷子大概分三块:基础题(语言、操作系统、网络、数据库)、算法与逻辑题(编程题或伪代码题)、专业方向题(按客户端、服务端、测试等岗位分卷或选答)。
当年不少同学挂在笔试上,不是因为题目多难,而是因为每道题都想写满,结果前面基础题磨了太久,后面算法题只能草草收场。这个题量设计本身就是筛选手段:研发岗的工作场景里,你永远面对的是优先级排序和资源分配,笔试也在模拟这个场景。会做和做完是两码事,做完和做对又是两码事,面试官通过这套题往往先看你的取舍能力。
1.2 题型构成:为什么这样分配
2016年前后,游戏公司的研发笔试有非常鲜明的行业特征。客户端岗位必考C++或C#,引擎相关(Unity、Cocos2d-x)的基础概念也会带一点;服务端岗位必考TCP/IP、Linux、并发编程、数据库事务与索引;所有岗位都必考算法题,哪怕岗位描述里没写。
从命题人的角度,这种分配背后的逻辑很清楚:研发工程师哪怕是应届生,入职三个月内就要在真实项目里干活,而游戏/互联网项目最大的特点是并发高、逻辑复杂、线上问题排查难。基础题负责过滤“基础不牢的”,算法题负责过滤“思维不够的”,专业题负责过滤“没做过功课的”。三层过滤,成本低、效果好。
1.3 2016年这个节点的行业背景
那年是什么情况?移动端流量红利还在,Unity开发已经成了客户端主流,Cocos2d-x还大量存在于2D游戏项目里;服务端技术栈以C++和Java为主,PHP在Web后端也还有相当份额;运维和SRE岗位还没完全分化。另一个背景是端游IP转手游的风口,完美世界的《诛仙》《完美世界》等IP都在往移动端迁,这就导致笔试里比较重视“游戏逻辑实现”和“状态同步/帧同步”的意识。
理解了这层背景,再看笔试题目就不会觉得有些题“偏”或者“奇怪”了,它考的不是通用计算机理论,而是“能在游戏研发环境里直接用上的计算机理论”。
2. 基础题部分:C++/语言与计算机基础的高频考点拆解
2.1 语言基础:指针、内存与“经典三连”
当年C++岗位的基础题里,几乎是必考栈和堆的区别、指针和引用的区别、深拷贝和浅拷贝的区别。别小看这三题,看起来是送分,实际上能全对的应届生不多。面试官很清楚,这三题对应的正是线上崩溃的三大来源:悬挂指针、内存泄漏、容器拷贝隐式开销。
栈和堆的区别,标准答法是:栈由编译器自动分配和释放,存储局部变量和函数调用信息,速度较快,容量有限;堆由程序员分配和释放,容量大,但分配和释放需要手动管理,可能产生碎片。这些基础能背下来不算本事,能把“栈上的对象出了作用域自动析构”和“成员函数里的临时对象在返回时发生拷贝”合在一起想,才算真的理解。
我当年在笔试里见过一道题,给出一段代码,里面写了一个返回局部对象引用的函数,问运行时会怎样。大部分人的第一反应是“编译报错”,但实际在当时的编译环境下是能编过的,运行结果则不可预期。这题的考点就是“引用指向栈上的悬垂对象”,你不但要知道合法不合法,还要能解释出“引用本身不持有对象生命周期”这个本质。
2.2 操作系统:进程、线程和那道“经典死锁题”
操作系统部分,完美世界那年的题比较偏向实战。进程和线程的区别属于必考,但问法很刁:会给出一个多线程服务器模型,问线程共享哪些资源、独享哪些资源、上下文切换开销为什么比进程小。
这道题其实考的是Linux线程的本质:在Linux内核中,线程和进程都是用task_struct描述的,线程是“共享地址空间和大部分资源的轻量级进程”。你能答出“线程栈独享、寄存器上下文独享,但代码段、数据段、堆、打开的文件表共享”,就已经能拿大部分分了。如果再能补一句“线程切换不需要切换页表,所以TLB命中率更高,开销自然小”,那这道题就是稳的。
死锁那题更经典。我记得问的是“线程A持有了锁1等待锁2,线程B持有了锁2等待锁1,如何避免”,很多人在笔试现场写出了“避免循环等待”五个字,然后没有然后了。正确写法是:至少说明三种常见手段——按固定的全局顺序加锁、使用trylock加超时回退、把多个锁合并成单一锁,并说明各自的适用场景。面试官要的从来不是标准答案,而是你有没有在实际编码中思考过“锁顺序”这件事。
2.3 网络:TCP三次握手和TIME_WAIT的实战含义
网题在游戏服务端岗的笔试里几乎占到基础题的半壁江山。最常考的还不是三次握手本身,而是“为什么握手是三次、挥手是四次”。这题的答法,决定了你在面试官眼里是背八股还是真懂。
三次握手的本质是“让通信双方确认彼此的收发能力都正常”。第一次握手,客户端确认自己没有发错目标;第二次握手,服务端确认客户端能发、自己能收;第三次握手,客户端确认服务端能发、自己能收。这比中文互联网上流传的“建立连接需要三次”要更本质。四次挥手则是因为TCP连接是全双工的,一个方向的FIN只能关闭该方向的传输通道,两方向各自关闭,自然最少四次。
还有一个高频考点是TIME_WAIT。游戏服务端长连接多、短连接也多,TIME_WAIT堆积会导致端口不足,线上经常抓瞎。笔试如果问你TIME_WAIT是干嘛的,标准回答是“确保最后的ACK能到达对端,同时让旧连接的迟到报文在网络中自然消亡”。加分回答是“高并发短连接场景下,可以通过调整tcp_tw_reuse和SO_LINGER等方式缓解,但要注意副作用”。你如果能把这两层都答出来,这道题基本就是往上拉分用的。
2.4 数据库:索引失效和事务隔离级别
服务端方向的卷子里会有一两道数据库题。2016年这个节点,MySQL还是主力,问的也多集中在索引。常见问法:给一条SQL,问它能不能命中索引,如果不能,为什么。
典型的坑有这几个:对索引列使用了函数或运算、隐式类型转换、左模糊查询、OR条件中有非索引列、联合索引不满足最左前缀。这些不需要死记,理解B+树的底层结构就能推出来。比如左模糊查询不能命中,是因为B+树的叶子节点是有序排列的,通配符在开头时无法确定扫描起点。
事务隔离级别那题,更侧重概念的对比。四大隔离级别——读未提交、读已提交、可重复读、串行化——分别解决什么问题,会导致什么问题。MySQL默认是可重复读(Repeatable Read),这一点很多背了八股的人会记反,因为Oracle默认是读已提交。笔试现场如果忘了,就从解决方案反推:“可重复读是保证事务内两次读同一行结果一致”,这样就很难记错。
3. 算法题:从“能跑”到“能过”的思考路径
3.1 2016年研发笔试的算法题单
算法题是整套卷子里区分度最高的部分,也是刷题党最有把握的部分。完美世界的笔试题在算法上不算“变态”,难度集中在LeetCode中等题偏上一点,基本不考ACM级别的硬核题。总结起来,当年出现在各家公司笔试题里的算法题有这些高频类型:
- 链表操作:反转、判环、找中间节点(快慢指针)
- 字符串处理:最长回文子串、模式匹配、字符统计类题目
- 数组与双指针:有序数组合并、三数之和、滑动窗口
- 二叉树:遍历、重建二叉树、最近公共祖先
- 动态规划:背包问题变种、最长递增子序列、编辑距离
- 排序与TopK:快排变体、堆排序、大数据量下的TopK问题
- 图论简单题:拓扑排序、最短路径(Dijkstra)
- 数学与逻辑:约瑟夫环、约塞夫问题、概率计算题
看到这个列表你会发现,它和今天的算法面试题库高度重合。原因很简单:这些题的思维模式是通用的,背的是模板,考的是思维迁移能力。
3.2 举例:一道大数据TopK题的三层递进思路
2016年的笔试题里,TopK问题几乎是不变的钉子户。最常见问法有两种:一是“从10亿个数中找出最大的100个数”,二是“从海量日志IP中找出访问次数最多的K个IP”。
以“10亿个数中找最大100个”为例,多数人第一反应是全排序,这是肯定不行的。正确的思考路径应该分三层依次推进:
第一层:数据量能不能全部读入内存?10亿个整数约4GB,在2016年普通开发机8GB内存的情况下还能勉强,但在笔试场景里应该默认不行。所以第一层思路是“不能全量排序,要局部淘汰”。
第二层:用最小堆维护K个元素。堆顶是当前K个元素中最小的,遍历数据时如果新元素比堆顶大,就替换堆顶并调整堆,最终堆里就是最大的K个。时间复杂度O(N log K),空间复杂度O(K),这是标准优秀解。
第三层:如果K也很大怎么办?比如K=1000万,堆也不合适了。那就分治,把数据切分成多块,每块分别建堆取TopK,然后归并。再极端一点,单机算不动就上多机,每台机器算本地TopK,最后合并各路结果。这层思路还能延伸到MapReduce的表达方式上。
很多人在笔试中只写到第二层就收工了,但面试官看到第三层,才会给你打高分。写题的时候不需要把代码写得多完整,把思路写清楚、复杂度算对、关键步骤标明,往往比一段冗长的完整代码更有价值。
3.3 解题过程的书写规范
纸笔考试场景下,算法题的书写规范直接决定你能拿多少过程分。我总结了一套顺序,适合绝大多数笔试场景:
第一步,先在草稿区写“思路”:用一两句话说明算法思想,比如“用哈希表记录每个字符最后出现的位置,双指针维护无重复窗口”。第二步,写“复杂度分析”:时间复杂度和空间复杂度各一行。第三步,再写核心代码。第四步,如果有边界情况,比如空字符串、只有一个元素、数字溢出,单独标一行注释。
这套顺序的价值在于,即使你的代码写得有瑕疵,面试官也能通过思路描述和复杂度分析判断你是真的会,而不是背了题。相反,代码写得飞快但没有任何思路说明的人,一旦出现笔误,很容易被误判为“不会”。
4. 专业方向题:客户端、服务端与测试岗位的差异
4.1 游戏客户端方向:渲染基础与Unity/Cocos考点
完美世界毕竟是游戏公司,客户端岗位的笔试题会明显偏向游戏技术栈。Unity相关的基础题通常包括:GameObject和Component的关系、MonoBehaviour生命周期函数的执行顺序、Prefab和实例的关系、资源加载方式(Resources和AssetBundle的区别)。
这些题回答起来不难,但要注意细节。比如生命周期函数,不仅要把Awake、OnEnable、Start、Update、LateUpdate、OnDisable、OnDestroy背下来,还要知道它们在不同场景下的触发时机:脚本被动态加载和场景初始化加载时,Awake和Start的调用顺序是否有区别?OnEnable在下一次激活时会不会再触发?这类细节才是拉开差距的地方。
渲染基础的题目也常见,比如“简述渲染管线的主要流程”。这个可以按阶段拆成:应用阶段(CPU提交渲染数据)、几何阶段(顶点变换、投影)、光栅化阶段(三角形到像素)、逐片元操作(深度测试、混合)。这题在2016年的笔试里出现频率非常高,因为手游刚大爆发,引擎团队需要那种“知道底层在干什么”的开发者,而不是只会拖拽GameObject的“组装工”。
如果岗位偏引擎或图形方向,还会出现矩阵变换相关的题目。比如模型坐标到屏幕坐标经历了哪几次变换(Model、View、Projection、Viewport),法线矩阵为什么要用逆转置矩阵。后者能答出来的人极少,但这道题能直接区分出“写过Shader”的人和“没写过”的人。
4.2 服务端方向:高并发、状态同步与数据一致性
服务端方向的题更“硬核”。除了基础题里占了大头的操作系统和网络,还要面对游戏逻辑和并发场景的结合题。
一个常考的场景题:一个游戏排行榜服务,日活跃用户百万,每小时有大量分数更新,需要实时排行,怎么设计?合理的结构应该包含几层:第一层,数据放Redis的有序集合(ZSet),以玩家ID为成员、分数为分值,天然支持按分数排名。第二层,写并发控制,用Lua脚本或事务避免排行榜分数更新的竞态条件。第三层,如果排行榜需要按多个维度(如战力榜、等级榜、伤害榜),每个维度一个ZSet,或加维度字段作为Redis Key的一部分。这道题考察的并不是Redis API背得多熟,而是“把业务需求翻译成数据结构”的能力,而ZSet的跳表实现刚好适合这种读多写少的场景。
另一个高频题是帧同步和状态同步的区别。客户端游戏里,MOBA和格斗类常用帧同步,MMORPG常用状态同步。笔试常见的问法是“各有什么优缺点”和“各自的适用场景”。要点是:帧同步要求所有客户端输入一致、逻辑一致,服务器带宽消耗小,但断线重连和反外挂难度高;状态同步以服务器为准,客户端的任何表现差异都允许,做起来也更省心,但服务器压力和带宽开销更大。2016年恰逢MOBA手游风口,这个问题问得特别多。
再有一个经典的数据一致性题是“商城购买流程如何避免超卖”。标准答法:在数据库层面用乐观锁(版本号)或悲观锁(SELECT FOR UPDATE);同时引入Redis的原子操作做库存预扣,再异步落库。如果你还能补充“超卖的本质是check-then-act的竞态窗口,要消除竞态只能让检查与扣减原子化”,那这道题你就稳了。
4.3 测试开发方向:用例设计不只看覆盖
完美世界也有测试开发岗,笔试侧重点完全不同。除了计算机基础一样要考之外,还会出“给一个场景设计测试用例”的题。
这类题最能看出一个人的测试思维。以“登录模块”为例,很多人能写出一堆“用户名正确密码正确、用户名错误密码正确”这类用例,但这只覆盖了最表层。真正高质量的用例设计至少包含:正常路径(正确账号和密码、记住登录状态、多终端登录被踢下线)、异常路径(密码错误、用户不存在、账号被锁定、验证码错误)、边界条件(账号长度边界、密码长度为0、账号含特殊字符)、安全与权限(SQL注入、暴力破解限制、异地登录提醒)、兼容性(不同浏览器、不同分辨率、弱网环境)。
我当时的经验是:笔试题里你不需要把所有用例都列全,但必须体现出分类层次。分维度写,比平铺写一百条用例更能拿分。测试岗笔试考的不是记忆力,是结构化思维。
5. 现场笔试的实战策略与考后复盘
5.1 时间分配策略:拿分优先,做完为先
根据我当年参加完美世界笔试和后来帮人复盘的共同经验,整套卷子时间分配可以按“6:3:1”的原则来。60%的时间给算法题和需要深想的专业题,30%给基础题里的操作用例和简答,10%留给复查和补充。
但这套原则有一个前提:先把整张卷子扫一遍。两分钟快速浏览全部题目,在心里给题目难度打标签。简单题绝对不丢分,中等题全力拿分,难题只花预留的少量时间,不留恋。这个顺序其实和面试官出题顺序没关系,但对你拿分最有帮助。我见过不少同学按题目顺序从头写到尾,最后算法题没时间写,这就是典型的本末倒置。算法题分值最重,永远值得你优先投入。
5.2 不会做的题,怎么处理才不吃亏
笔试现场一定会遇到不会的题,关键是别让它拖垮你的心态和总卷面。处理方法有两条经验。
第一,不会的题也把基础框架写出来。比如一道动态规划题,状态定义想不清楚,那就把dp[i]的含义先写出来,把状态转移方程写一个“疑似正确”的版本,把边界条件列出来。哪怕最终方程不对,面试官也能看到你有DP的解题框架意识,这会比留白强很多倍。第二,标注出“我认为这题的薄弱点在于xxx”,这种自我认知的展现对研发岗来说反而是加分项。在真实项目里,你不需要永远都知道答案,但需要快速地定位自己“卡在哪一步”,笔试如果能把这一步标出来,面试官会认为你有工程调试的潜力。
5.3 考后复盘:从一套题里抽出可复用的知识树
笔试结束不代表这事结束了。聪明的做法是当天趁热打铁做复盘。不完全是为了这家公司的面试流程,而是因为这套题本身能帮你检查自己知识树的漏洞。
我建议复盘时按这个模板整理:第一,做错的题和不会的题,各自对应哪个知识模块(C++内存?TCP状态?MySQL索引?DP模型?);第二,这个模块里的题是“不会”还是“不熟”,不会的要补全知识点,不熟的要加强练习频率;第三,自己答得最好的一道题是哪道,为什么答得好——是刷过类似题、还是知识点理解深、还是临场推理快。
这套复盘模板的价值在于,它把一次笔试从“被筛选”变成“自我诊断”。哪怕这家公司没给你面试机会,你这份复盘也是你下一场笔试前的第一手复习资料。
5.4 那些笔试之外同样重要的细节
还有几个细节,很多人事后才追悔莫及。一个是环境问题,当时机考的话,提前确认编译器版本和代码输入输出模板,C++写惯了Linux下g++,结果笔试系统里是Windows的VC6,遇到这种情况心态容易崩。另一个是题目没有明说的地方:它考的是“伪代码即可”还是“完整可运行代码”,这决定了你在时间分配和书写粒度上的选择。还有一个是代码风格,笔试时虽然不要求写注释,但你至少要把变量命名写得能看懂,因为面试官会拿着你的笔试卷子来看,如果自己都认不出自己写的是什么,这场笔试白考了。
我个人每次建议别人准备笔试时,都会加一条:找一套往年真题,把它当成真实的笔试来做,闹钟定时、不查资料、按正式要求写完一整套。做完之后你心里对自己的水平和节奏就有了底。很多人不是知识储备不够,而是第一次进笔试考场不知道题目长什么样,临场慌了。用真题模拟一次,这个坑就提前踩掉了。