
聊到美团2016年的研发工程师模拟笔试题我翻了翻当年整理的题库发现这些题哪怕放到今天考察的底层功底依然不过时。美团校招笔试题向来有个特点量不小、偏向实战、算法和工程结合得很紧不会让你死背概念而是看你用代码解决真实问题的能力。这篇文章我把当年那份模拟题按题型拆开结合我后来做面试官的经验把每道题背后的考察点、解题思路和容易踩的坑都捋一遍。适合正在准备大厂研发岗位笔试的应届生也适合想自查基础功底的初中级工程师。题目不区分具体语言代码示例用Java为主但思路是通用的。1. 整体设计与考察思路拆解1.1 2016年那个时间点的笔试题背景先交代一下当时的技术背景。2016年美团的技术栈正处在快速演进期后端大量使用Java部分老系统还是PHP同时移动端业务爆发对服务端的并发处理、稳定性、容灾能力要求很高。所以笔试题的导向非常明显考察候选人的算法功底是否扎实、对数据库和缓存的理解是否到位、写出来的代码是否具备生产环境的工程素养。整套模拟题的时间一般是90分钟到120分钟题量大概分为四个部分20道左右的基础选择题、2到3道手写算法题、1道SQL题、1道场景设计题。选择题覆盖网络、操作系统、数据库、Java基础每题虽然只有一分但拉分很快很多人挂在细节上。手写算法题是重头戏占分最多而且题目大多来自LeetCode中等难度但有一个区别美团喜欢把算法套进业务场景里比如“外卖配送员路径”“用户订单去重”这种不会直接问裸题。我看到很多候选人刷题只刷题干不看场景结果碰到稍微包装一下的题目就懵了。这是值得先说的一点美团笔试考算法但考的是你如何在约束条件下把算法落到代码里。1.2 为什么这套题值得反复做这套题的参考价值在于它很典型地代表了“大厂研发工程师”的基础要求。尤其是以下几个能力维度对数据结构本质的理解而不只是背模板对并发的敏感度能否写出线程安全且不过度加锁的代码对数据库原理的掌握比如索引失效场景、事务隔离级别对系统设计的思路面对不确定需求时能否做出合理假设。这些能力不是靠记忆能获得的必须通过刷题加复盘逐步建立。我当年做这套模拟题时第一遍算法题只做对一半后来把每道题的边界条件、复杂度推导自己重写了一遍再面对同类题就顺了很多。2. 基础选择题考点覆盖与易错点精讲2.1 网络知识TCP连接断开为什么要TIME_WAIT选择题里网络部分基本必考TCP尤其是三次握手和四次挥手的状态变化。有一道典型的题主动关闭连接的一方在发送最后一个ACK后为什么要进入TIME_WAIT状态并且等待2MSL很多人的回答停留在“为了保证最后一个ACK能到达对端”这不算错但不完整。核心有两个作用第一保证最后一个ACK丢失时能重发因为如果对端没收到ACK会重发FIN而主动关闭方只有在TIME_WAIT状态下才能处理这个重发的FIN第二让旧连接的报文段在网络中自然消失避免新连接收到旧连接的延迟数据。如果题目再深挖一层比如“为什么是2MSL而不是别的值”就需要知道MSL是报文段最大生存时间2MSL确保了一个方向上报文段的最大存活时间加上对端ACK的最大存活时间这样新旧连接不会串数据。这类细节我建议复习时以“状态迁移图场景推演”的方式理解死记硬背很容易忘记。2.2 操作系统进程线程模型与上下文切换开销另一道高频选择题进程和线程的根本区别是什么很多选“线程比进程轻量、切换快”的人对但在面试官眼里不算本质。本质区别在于进程是系统资源分配的基本单位线程是CPU调度的基本单位。进程拥有独立的地址空间线程共享进程的地址空间。因此线程切换不需要切换页表、不需要刷新TLB开销小但线程安全问题也因此产生。题目如果换个问法“哪些资源是线程私有、哪些是共享的”答案就要按栈、寄存器私有、堆、全局变量、文件描述符共享来梳理。我当时复习这块用的办法是画一张进程和线程的资源对比表把地址空间、栈、寄存器、全局变量、文件描述符、信号处理状态逐项打勾比纯读十遍书都管用。2.3 数据库隔离级别与并发问题对应数据库是选择题里最容易出细节题的部分。有一条经典的对应关系读未提交会脏读读已提交解决了脏读但存在不可重复读可重复读解决了不可重复读但存在幻读串行化全部解决。美团喜欢问MySQL在默认级别下的行为InnoDB默认是可重复读而且通过Next-Lock Lock解决了部分幻读问题。注意这里说的是“部分”不是全部MySQL的可重复读并不能完全消灭幻读尤其是在当前读和插入操作混在一起的场景下如果没有正确加锁仍然可能出现幻读。做题时如果选项里出现“MySQL可重复读级别没有幻读”那基本是错的。这个点是大多数参考书不强调的但面试官经常拿来出题。2.4 Java集合与并发容器Java方向的岗位HashMap基本是必考。2016年那会儿流行的问法还是JDK7和JDK8的HashMap区别JDK7用数组加链表扩容时头插法可能导致死循环JDK8改成尾插法并引入红黑树链表长度超过8且数组长度超过64时转红黑树缓解了哈希冲突下的性能退化。选择题里可能这样问HashMap在并发put时会出现什么现象选项有“数据覆盖”“死循环”“扩容异常”“抛ConcurrentModificationException”。正确答案是数据覆盖和扩容异常都可能JDK7头插法下还可能出现死循环但HashMap本来就不是线程安全容器任何并发问题都属于“未定义行为”。如果题目里出现ConcurrentHashMap要分清JDK7的Segment分段锁和JDK8的CAS加synchronized锁桶后者在锁粒度和并发度上都有优化。3. 手写算法题三道典型题的完整拆解3.1 判断单链表是否有环并找到环的入口这题是美团笔试和面试里出现频率极高的题目几乎可以算“送分题”但送分不代表你能拿满分。题目描述给定一个单链表判断是否有环如果有请返回环的入口节点。常规做法是快慢指针慢指针每次走一步快指针每次走两步。如果快指针走到null说明无环如果快慢指针相遇说明有环。关键是找到环入口的数学推导设头节点到环入口的距离为a环入口到相遇点的距离为b环剩余长度为c那么环长为bc。慢指针走了ab快指针走了abk(bc)。因为有2倍关系所以ab和k(bc)之间满足一个同余关系最终可以推导出从相遇点和头节点同时各走一步它们会在环入口相遇。代码写起来并不复杂但有几个坑要注意链表可能为空、只有一个节点无环、快慢指针初始都从头节点出发。边界条件没处理好很容易在链表很短时越界或空指针。public ListNode detectCycle(ListNode head) { if (head null || head.next null) return null; ListNode slow head; ListNode fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { ListNode ptr head; while (ptr ! slow) { ptr ptr.next; slow slow.next; } return ptr; } } return null; }时间复杂度和空间复杂度都要写清楚时间是O(n)空间是O(1)。如果在笔试现场只写出了哈希表解法O(n)空间也能过测试用例但面试官追问优化方案时就会露馅。3.2 拓扑排序课程安排是否可行这题是典型的图算法套业务场景。题目大意有n门课程编号0到n-1给定若干二元组表示课程之间的先修关系比如[1,0]表示要学课程1必须先学课程0请判断能否修完所有课程。这道题本质是判断有向图是否存在环。解题首选拓扑排序使用Kahn算法先统计每门课的入度并把入度为0的课程入队每次从队列取出课程将其所有后继节点的入度减1如果减到0就入队。最后统计取出的课程数是否等于n不等于则说明存在循环依赖。实现时还有一个容易踩的坑用邻接表时后续节点的去重。如果题目给的边存在重复入度会重复累加导致错误的判环结果。稳妥的做法是建图时同时检查重复边或者直接用Set结构存储邻接表。3.3 动态规划最长上升子序列美团笔试常出这类题因为外卖调度、订单排序等业务中很常见。题目给定一个无序整数数组求最长严格递增子序列的长度。经典O(n²)解法对每个位置i遍历之前所有位置j如果nums[j] nums[i]dp[i] max(dp[i], dp[j] 1)最终答案是dp数组的最大值。但如果笔试时间剩余充足O(nlogn)的贪心加二分解法更能体现功底维护一个tails数组tails[k]表示长度为k1的递增子序列的末尾最小元素遍历每个数时在tails中二分查找第一个大于等于它的位置并替换。我在笔试时建议先写出O(n²)的解法保证正确性再根据剩余时间决定是否优化。因为有些判题系统不看重最优复杂度只要求通过测试用例而在时间紧张的情况下写出优化解反而可能因为二分边界出错而失分。public int lengthOfLIS(int[] nums) { int[] tails new int[nums.length]; int len 0; for (int num : nums) { int i 0, j len; while (i j) { int mid (i j) 1; if (tails[mid] num) { i mid 1; } else { j mid; } } tails[i] num; if (i len) len; } return len; }注意二分查找时的边界查找目标是第一个大于等于num的位置。这里用j mid而不是j mid - 1就是为了保证能找到最左侧的插入点。很多人在这里写错。4. SQL题订单场景下的查重与聚合4.1 题目查询每个用户最近一笔订单的完整信息表结构一般这样给用户表users(user_id, user_name)订单表orders(order_id, user_id, order_time, amount)。要求查询每个用户最近一笔订单的订单号、下单时间和金额。这道题考SQL基本功也考逻辑能力。最直观的写法是先按user_id分组查最大order_time再关联回原表。这个写法能过但在订单表数据量极大的情况下性能不理想而且如果同一个用户在同一个时间点有多笔订单关联会有重复。更严谨的写法是使用窗口函数SELECT user_id, order_id, order_time, amount FROM ( SELECT user_id, order_id, order_time, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders ) t WHERE rn 1;用ROW_NUMBER可以处理“同一时间多笔订单”的场景按业务需求选择保留哪一条。2016年MySQL还没有窗口函数MySQL 8.0才引入所以当时标准答案是自连接写法。现在如果笔试环境允许用8.0窗口函数是更优解。4.2 索引失效场景与优化思路SQL题里还经常混一道选择题在什么情况下索引会失效典型场景包括对索引列使用函数或运算、隐式类型转换、like以通配符开头、使用OR连接非索引列、联合索引不满足最左前缀原则。我做过一个实际案例订单查询接口传入的“下单时间”是字符串SQL里直接写where order_time 2024-01-01 00:00:00假设order_time是datetime类型MySQL会把字符串转成datetime再比较这时索引可以用。但如果反过来把字符串和datetime列比较时发生隐式转换索引就可能失效。笔试做题时记住一个核心判断方式索引列本身是否被“加工”了。列上套函数、列和不同类型比较、列参与运算都会让优化器放弃索引。至于like %xxx是因为无法利用B树有序性进行范围匹配。4.3 慢查询排查思路引申美团这种重视线上稳定性的公司SQL题背后往往隐藏着慢查询排查的考察。我在实际工作中遇到过一条线上SQL调优的案例订单表超过千万行按user_id和order_time的联合索引查询用户最近订单本来应该走索引但因为查询条件里对order_time用了DATE_FORMAT函数导致索引失效全表扫描拖垮了数据库。排查方式也很标准先用EXPLAIN查看执行计划看type字段是ALL还是ref、range再看key字段实际使用了哪个索引接着分析extra字段里有没有Using filesort或Using temporary。这套思路在笔试的场景设计题里也可以当作得分点来写比单纯回答SQL语法更让面试官印象深刻。5. 系统设计题短URL生成与接口签名方案5.1 设计一个短URL服务系统设计题通常不会要求你写出完整代码而是考察你能否拆解需求、设计存储、考虑并发和容错。短URL服务是一个非常经典的题目美团也考过类似的。拆解下来要做四件事生成短码、存储映射关系、重定向跳转、处理高并发。生成短码最常用的方案有两种。一种是发号器用一个全局自增ID然后转成62进制字符串优点是短码唯一而且能反推出顺序缺点是依赖发号器单点需要考虑高可用。另一种是随机生成短码然后查重优点是简单但碰撞概率和重试成本需要控制。我推荐用“发号器 短码映射”的组合先通过数据库自增ID或分布式ID生成一个唯一数字ID再用62进制编码转成6到7位短码。这个方案的好处是短码无需查重因为ID本身就唯一。发号器的单点问题可以用多段发号器解决比如让每个节点发不同号段。存储层面短码和原URL的映射关系用Redis做缓存热点数据MySQL做持久化存储。写入时先写数据库生成ID再异步写到缓存读取时先查缓存缓存未命中再查数据库并回填。5.2 接口签名设计防篡改与防重放这个题目正好可以关联到美团业务接口里的签名机制本质上是在考一个很通用的技术点如何保证接口请求的安全性。面试官给的常见场景是开放平台给第三方开发者提供API调用方需要携带参数调用接口服务端如何确认参数没有被篡改、请求不是重放的答案思路是这样调用方和平台约定一个AppSecret调用时把请求参数按字典序拼接加上时间戳和一个随机数nonce然后用MD5或HMAC-SHA256计算签名把签名放在请求头里。服务端收到请求后用相同算法计算签名并比对同时检查时间戳是否在允许的偏移范围内比如5分钟并校验nonce是否已经用过来防重放。这里面有几个容易被忽略的细节。时间戳偏移量如果设置太大重放攻击的窗口期变长如果太小客户端和服务端时钟不同步会导致误杀。nonce必须结合时间戳使用否则服务端要存的nonce列表会无限增长。更好的方案是nonce只保留一个时间窗口内的数据过期可以删除。// 伪代码生成签名 String raw appIdxxxparam1value1param2value2timestamp1720000000nonceabc123; String sign hmacSha256(raw, appSecret);服务端校验签名可以看作分布式系统中“数据完整性”的一个应用场景。回答时如果能把这些细节都覆盖到面试官会认为你有线上安全意识。5.3 秒杀系统设计的几个关键点如果笔试里出现高并发场景设计大概率是秒杀或抢购这类题目。核心难点不在于数据库读写而在于流量控制。我的回答框架分了四层。第一层是前端限流比如按钮置灰、随机延迟把大部分无效请求挡在前面。第二层是网关层基于令牌桶做接口限流超出的请求直接返回失败或排队。第三层是应用层用Redis预扣库存只有扣减成功的请求才真正走数据库下单减少数据库压力。第四层是数据库层用乐观锁或悲观锁保证最终一致性。很多候选人把精力花在“怎么把数据库写快”上方向反了。秒杀系统的核心思路是把瞬时流量削峰让数据库只处理有限的有效请求。6. 常见问题与避坑技巧实录6.1 笔试现场时间不够怎么办我见过太多人栽在时间分配上。选择题里纠结一道网络题十分钟最后算法题没写完。我的策略是先把所有题目快速浏览一遍遇到没思路的题先标记跳过选择题控制在30分钟内完成SQL题10分钟剩下的时间全留给算法题和设计题。算法题里先做有把握的把暴力解写上去保证得分再回头优化。6.2 边界条件和复杂度分析不能省写完代码只是第一步你必须主动写出边界条件和复杂度分析。笔试的阅卷人往往根据这些信息判断你的工程能力。比如链表题要写清“链表为空时返回null”“只有一个节点时如何处理”复杂度要写清时间和空间最好是O(n)、O(1)这种精确表达而不是“很快”“很少”这种模糊说法。6.3 SQL题里最容易被忽略的重复数据SQL题常见丢分点是没考虑数据重复。比如查询每个用户最近订单同一个用户在同一秒内下两单直接用group by max(order_time)再join会把两单都查出来这就不符合“每用户一条”的要求。用窗口函数加rn1可以保证只取一条。笔试时如果题目没有明确说明数据唯一默认要考虑重复场景。6.4 手写代码时的细节习惯我在现场阅卷时经常看到的代码问题包括方法命名不规范、变量名用单字母且无注释、没有空指针防御、循环里重复计算长度。笔试不是考你写出机器能跑的代码就行阅卷人看你代码的习惯会推测你写生产代码的样子。常见的提升方法是写代码前先声明入参和返回值然后用注释标出核心逻辑步骤。不用写得很啰嗦但在关键算法步骤留一两行注释能大幅提升阅卷体验。7. 这套题想真正“吃掉”复盘比刷题更重要我自己把这套模拟题总结成了一段话算法题考察的是数据结构和数学推导SQL题考察的是逻辑严谨性和索引意识设计题考察的是需求拆解和架构权衡选择题则是在检验你平时积累的深度与广度。每类题都有固定的套路但只有复盘才能把这些套路内化成自己的解题本能。推荐的做法是准备一个刷题记录表把每道题错的点、下次需要注意的边界条件、复杂度方案都写下来。过两周再把这些题重做一遍如果还能流畅写出正确的解题思路和复杂度分析这道题才算真正掌握了。笔试的题目其实是有穷的反复吃透真题和模拟题比漫无目的地扩充题海有效得多。