尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

美团2016研发工程师笔试题解析:从数据结构到算法的核心考点复盘

美团2016研发工程师笔试题解析:从数据结构到算法的核心考点复盘 在2016年这个时间节点美团正处于“千团大战”后的高速上升期技术团队对研发工程师的筛选已经形成了一套非常标准的流程。现在回看那份“美团2016研发工程师笔试题(二)”其实它不只是一套过期考题更像是一份浓缩的考点图谱把计算机基础中那些“看似简单、实则暗藏杀机”的知识点全部串了一遍。哪怕放在今天这套题的参考价值依然很高尤其是准备校招或跳槽去互联网中大型公司的朋友很有必要拿它当一次摸底自测。我当初刷这套题时最大的感受是美团的笔试不故意刁难人但非常讲究基础功的牢固程度。题目没有离谱的偏题怪题绝大部分都是学科内部的核心知识点。如果你能把这套题做到85分以上的正确率说明数据结构、算法设计、操作系统和计算机网络这几块的基础是扎实的。下面我从考题背后涉及的考点出发结合我自己的做题体会和复盘经验做一个完整的拆解。1. 这份题的价值与考点全景1.1 2016年的美团笔试在考什么2016年是移动互联网红利最明显的时候美团的核心业务已经从团购扩展到外卖、酒旅、电影等本地生活服务全品类。在这个阶段研发工程师笔试的重点非常清晰不考花哨的框架知识不考新兴技术名词而是考计算机专业最核心的主干基础。因为当时团队扩张速度快面试官最看重的是候选人能不能在快速变化的业务中靠扎实的底层功底快速上手。这套笔试题的考察范围可以用四个字概括广而深。广是指覆盖了数据结构、算法、操作系统、网络、概率统计等核心学科深是指同样的考点下出题人喜欢在边界条件和复杂度分析上做文章单纯的“会做”不够还要做对、做快。我当时做完这套题有一种感觉它不像是在考八股文背诵而是在考你“遇到一个真实问题时能不能用最合适的数据结构和算法把它解出来”。比如二叉树的遍历、堆排序、动态规划这类经典题目美团虽然在2016年就考了但每一道题背后都能延伸到实际业务场景。1.2 考点模块与题型分布从试题的整体结构来看主要分为客观题和编程题两大块。客观题部分重点考察以下模块数据结构与算法时间复杂度分析、二叉树遍历、排序算法、哈希表、链表操作这些是绝对的主力大概占了客观题的40%以上。操作系统进程与线程、死锁、内存管理考得比较基础但经常设坑。计算机网络TCP协议、HTTP状态码、TCP三次握手等基础知识点题量适中。概率统计与逻辑推理偶尔会出现一两道概率计算题考察候选人的数学思维。编程题通常会有2到3道手写代码题重点集中在链表、二叉树、动态规划这类高频算法题上。从题型分布可以看出这份试卷的出题逻辑是“基础为主应用为王”。纯粹的背诵型题目很少大多数题目都需要你先理解原理再动笔计算或推理。1.3 站在今天看这套题还合不合时宜我知道很多人看到“2016年”这个年份第一反应是题目太老了可能不适合当前的技术面试。但我想说这个观点只对了三分之一。算法和数据结构的核心原理比如二叉树的遍历、快排的时间复杂度、动态规划的转移方程十年二十年都不会变。现在美团、阿里、腾讯等大厂的笔试题目虽然题目包装越来越新颖底层考点依然逃不出这些范畴。只是现在的题目更擅长“穿马甲”比如把哲学题、游戏题、工程场景题与算法结合但剥开外壳后考察的还是那几类基础算法。所以这套2016年的真题我建议这样用先不看答案完整做一遍记录每个模块花了多少时间再对照答案仔细复盘错题最后统计出自己在哪个知识板块最薄弱。这个过程比单纯刷三套新题更有效因为它能帮你定位问题而不仅仅是积累题目量。2. 选择题类考点逐个拆解2.1 时间复杂度分析别被循环嵌套的表象骗了那套题里有一道非常经典的时间复杂度题问的是下面这段代码的时间复杂度是多少int i 0, j 0; while (i n) { j 0; while (j n) { // 执行某些操作 j; } i i * 2; }如果你只看表面可能会觉得外层循环一次内层循环n次所以总复杂度是内外层相乘。但这个题的“坑”就在外层循环步长上i i * 2意味着外层变量不是线性增长而是指数级增长。外层循环实际上只执行了logn次所以整体时间复杂度是O(nlogn)而不是O(n²)。这道题给了我一个很重要的提醒分析复杂度的第一件事永远是看循环变量的变化方式而不是循环嵌套的层数。很多人在快排、归并这类分治算法中能轻松说出O(nlogn)但一到实际代码就容易被循环的增量方式迷惑。做这类题有一个系统性方法先把循环变量写出来再计算每一层循环的迭代次数最后统一用大O记号表达。对于嵌套循环优先看内存循环的执行总次数再看外层循环的迭代次数两者相乘就是整体复杂度。如果某一层循环变量是指数变化乘2、乘3这层循环的次数通常是对数阶。2.2 哈希冲突负载因子和冲突策略的选择哈希表是笔试常客美团这套题里有一道关于哈希冲突的题考察的是不同冲突处理策略的优劣。常见的冲突处理有开放定址法和链地址法两种而在开放定址法中又有线性探测、二次探测、双重散列等实现方式。实际业务中链地址法因为实现简单、内存管理灵活是Java HashMap默认使用的方案。而开放定址法因为缓存命中率高在某些高性能场景下更受欢迎。这道题考察的核心概念是负载因子load factor哈希表中已存储元素个数与哈希表长度的比值。当负载因子过高时冲突概率会急剧上升哈希表性能会退化。我当时答这道题时专门对比过不同负载因子下的性能表现实测数据是负载因子在0.5到0.75之间时哈希表的插入和查询性能最好超过0.75后冲突率明显上升尤其在链地址法下链表长度增加会导致查询从O(1)退化为O(n)。这里有一个实际工程中的经验扩容时机不能只看负载因子还要考虑单个桶的链表长度。Java 8中HashMap引入的红黑树优化就是当链表长度超过8且数组长度超过64时就把链表转成红黑树将最坏查询时间从O(n)降到O(logn)。做这道题时如果能提到这个优化思路面试官会认为你对哈希表的理解不是停留在教科书层面而是真正做过工程实践。2.3 堆与优先队列TopK问题的底层兵器美团这套题里有一道关于堆的题考察堆排序的时间复杂度以及堆在TopK问题中的应用。堆是一个完全二叉树分为大顶堆和小顶堆两种。堆排序的时间复杂度是O(nlogn)空间复杂度是O(1)属于原地排序算法。当时我在这道题上吃过亏因为我把堆排序的建堆过程记错了。建堆有两种方式一种是逐个插入时间复杂度是O(nlogn)另一种是从最后一个非叶子节点开始向下调整时间复杂度是O(n)。后者才是真正高效的建堆方式。笔试中如果让你计算建堆复杂度答案是O(n)而不是O(nlogn)。堆在面试中更高频的应用是解决TopK问题。在一个包含n个元素的数组中找到最大的K个数最直接的做法是排序后取出前K个时间复杂度O(nlogn)。但用堆可以把复杂度降为O(nlogK)。具体做法是维护一个大小为K的小顶堆遍历数组时如果当前元素比堆顶大就替换堆顶并调整堆如果比堆顶小就直接跳过。遍历结束时堆中保存的就是最大的K个数。这个方案在K远小于n时性能优势非常明显。我当时在一家公司的笔试中遇到过一个变种题数据量达到亿级内存放不下怎么求前100大的数。思路完全一致用堆离线处理即可。2.4 字符串匹配暴力解法之外的第一层优化字符串匹配的题目在笔试卷里非常常见美团这套题里也有一道。题目不是让你实现KMP而是判断一个字符串是否为另一个字符串的子串并计算朴素的暴力匹配法的复杂度。暴力匹配法的思路是从主串的第一个字符开始依次与模式串比较如果不匹配主串指针回退到起始位置的下一个字符继续匹配。最坏情况下时间复杂度是O(n*m)其中n是主串长度m是模式串长度。这个最坏情况只有在主串和模式串高度相似时才会出现比如主串是“AAAAAAAB”模式串是“AAAAB”每次都匹配到最后一个字符才发现不匹配。提到字符串匹配就绕不开KMP算法。KMP的核心思路是利用模式串自身的信息在匹配失败时不回退主串指针而是根据next数组跳转到模式串的某个位置继续匹配。这样时间复杂度降为O(nm)。虽然现在工程中很少人从头手写KMP但理解这个算法的思想能帮你建立“预处理模式串用空间换时间”的思维方式。如果你准备笔试时间有限我建议字符串匹配这块优先掌握暴力匹配代码能5分钟完成、KMP的next数组能手动推导、知道KMP时间复杂度为什么是O(nm)。掌握了这三项不管题目怎么变基本都能应对。3. 编程题实战三道必练的经典题3.1 链表反转迭代与递归两条路线链表反转是程序员面试的“题目之王”美团2016年的笔试二中同样出现了这道题。它考察的核心知识是是否理解指针/引用的传递逻辑是否能在不借助额外空间的情况下调整节点的next指向。迭代法是最容易理解的解法。需要维护三个指针prev指向前一个节点curr指向当前节点next指向当前节点的下一个节点。每次迭代中先把next保存下来再把curr的next指向prev然后把prev和curr分别前移一位。这里最容易被忽略的是必须先保存next再修改curr的next否则会丢失后续节点。ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* curr head; while (curr ! nullptr) { ListNode* next curr-next; curr-next prev; prev curr; curr next; } return prev; }递归法的代码更短但理解难度更高。递归法的本质是假设后面的链表已经反转好了只要处理当前节点和后续节点的关系即可ListNode* reverseList(ListNode* head) { if (head nullptr || head-next nullptr) { return head; } ListNode* newHead reverseList(head-next); head-next-next head; head-next nullptr; return newHead; }递归法的关键点是递归终止条件以及head-next-next head这一句。当递归返回时newHead就是反转后链表的头节点而head-next指向的是原链表的下一个节点这一步操作把当前节点接到新链表的末尾。我建议笔试时优先用迭代法因为不容易栈溢出也不容易在递归思想上绕晕。如果面试官要求用递归实现再转换思路。另外这道题有一个常见的变体——反转链表的第m到第n个节点难度会高一档但核心思路仍然是找到位置后进行局部反转笔试前可以一起练掉。3.2 用两个栈实现队列数据结构的组合技巧这道题在2016年美团的笔试题里出过现在依然是各大小公司的经典面试题。题目要求定义两个栈实现队列的push和pop操作并完成对应的复杂度分析。思路并不复杂用两个栈stackIn和stackOut入队时直接push到stackIn出队时先检查stackOut是否为空。如果stackOut为空就把stackIn的所有元素依次弹出并入栈stackOut然后从stackOut弹出栈顶元素。如果stackOut不为空直接从stackOut弹出即可。class MyQueue { private: stackint stackIn; stackint stackOut; public: void push(int x) { stackIn.push(x); } int pop() { if (stackOut.empty()) { while (!stackIn.empty()) { stackOut.push(stackIn.top()); stackIn.pop(); } } int result stackOut.top(); stackOut.pop(); return result; } };这道题的复杂度分析很有讲究。单次pop操作最坏情况是O(n)因为需要把n个元素全部倒到stackOut中。但均摊复杂度是O(1)因为每个元素最多从stackIn进入stackOut一次后续的所有pop操作都只需要弹栈即可。这就是工程中常说的“摊还分析”思想。这道题还有一个反向变体用两个队列实现栈。思路就完全不同了。用两个队列实现栈时需要在push阶段就调整元素的顺序保证队头一直是最后入队的元素。我当时第一次做反向变体时卡了很久后来总结出一个规律栈和队列的互换题目核心就是确定“谁负责存谁负责倒腾”。两个栈实现队列stackIn负责存stackOut负责倒两个队列实现栈则需要在push时反复腾挪确保最新的元素在队头。3.3 求数组第K大的元素堆解法与快选解法的取舍求一个无序数组中第K大的元素是一道非常经典的题目也是美团笔试题中频繁出现的高频题。这道题考察的不仅是算法本身更是对时间复杂度和实际场景的理解。解法一先用堆排序或快排全部排序再取第K个元素。时间复杂度O(nlogn)优点是好写、不易出错缺点是完全没有利用“只需要第K大”这个额外信息。解法二使用小顶堆维护当前最大的K个元素遍历数组最后堆顶就是第K大的元素。时间复杂度O(nlogK)空间复杂度O(K)。这个方案在K较小时非常高效比如数据量很大但K很小的情况。解法三基于快速排序的partition思想。每次partition后基准元素的位置就是它在最终有序数组中的位置。如果基准元素的位置恰好是K-1第K大对应升序排序中的下标n-K直接返回如果位置小于目标位置继续在右侧寻找如果大于目标位置继续在左侧寻找。平均时间复杂度O(n)最坏时间复杂度O(n²)。可以通过随机选择基准元素来避免最坏情况。int findKthLargest(vectorint nums, int k) { int left 0, right nums.size() - 1; int target nums.size() - k; while (left right) { int pivotIndex partition(nums, left, right); if (pivotIndex target) { return nums[pivotIndex]; } else if (pivotIndex target) { left pivotIndex 1; } else { right pivotIndex - 1; } } return -1; }这里有一个经验之谈在笔试场景下如果题目没有对时间复杂度做严格要求稳妥起见优先用堆解法或直接排序因为快选法的partition代码写错一个边界条件就很难调试。但在面试场景下我强烈建议你展示快选法并主动说明“期望是O(n)最坏退化为O(n²)可以通过随机化来优化”。这会让面试官认为你既有工程思维也有算法深度。4. 操作系统与网络基础容易被忽视的送分题4.1 TCP三次握手与状态变迁网络部分的考察在美团的笔试中不算难但非常经典。三次握手几乎是必考题目。这里需要注意的不只是三次握手的过程还有每次握手后连接的状态变化。我第一次复习时总是记混后来用一条时间线来帮助记忆第一次握手客户端发送SYN报文状态从CLOSED变为SYN_SENT。第二次握手服务端收到SYN回复SYNACK状态从LISTEN变为SYN_RCVD。第三次握手客户端收到SYNACK后发送ACK状态变为ESTABLISHED服务端收到ACK后状态也变为ESTABLISHED。笔试中如果考状态变迁大概率会围绕“SYN_SENT、SYN_RCVD、ESTABLISHED”这三个状态做文章。这里有一个延伸知识点为什么是三次握手而不是两次因为在不可靠的网络上引入三次握手可以防止历史重复SYN报文导致的连接建立错误。两次握手的话服务端无法确认客户端是否收到了自己的SYNACK就会导致浪费资源。除了三次握手TCP四次挥手也是高频考点。与握手不同挥手需要关注TIME_WAIT状态。主动关闭方在发送最后一个ACK后会进入TIME_WAIT并且要等待2MSL最大报文生存时间的两倍才能彻底关闭。很多候选人能背出TIME_WAIT但不明白为什么需要它。原因有两个一是确保最后一个ACK能到达对方如果丢了对方会重发FIN二是让旧连接的所有报文在网络中自然消逝避免污染新连接。这个细节是在大厂笔试中的加分项建议一定理解到位。4.2 进程与线程的灵魂三问进程与线程的区别是操作系统模块的必考题美团笔试中也出现了相关选择题。我通常建议从“资源拥有者”和“调度单位”这两个维度来理解进程是资源分配的基本单位每个进程都有独立的地址空间、打开的文件表、信号处理器等资源。线程是CPU调度的基本单位同一进程内的线程共享该进程的地址空间和文件资源但每个线程有独立的栈空间和寄存器上下文。笔试中最常出现的考点是进程间的通信方式有哪些、线程间共享什么不共享什么。进程间通信方式包括管道、消息队列、共享内存、信号量、套接字等共享内存是效率最高的方式但需要同步机制配合。线程间共享的是进程的堆、全局变量、文件描述符不共享的是栈、寄存器、程序计数器。这里有一个容易混淆的点很多人以为切换线程的代价一定比切换进程小。严格来说线程切换的代价确实小于进程切换因为线程切换不需要切换地址空间不需要刷新TLB但线程切换仍然涉及内核态与用户态的切换代价并不是零。这道题如果作为选择题出现最稳妥的选项是“线程切换比进程切换轻量但不等于没有代价”。4.3 死锁的四个必要条件与破解思路死锁是操作系统模块的另一个常青考点。死锁产生的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。这四个条件同时满足时才会发生死锁。考题通常有两种出法一种是让你判断一个场景是否会发生死锁另一种是让你回答如何预防死锁。预防死锁的策略本质上就是破坏四个必要条件中的任意一个。比如“破坏持有并等待”的做法是要求进程一次性申请所有资源申请不到就全部释放“破坏不可剥夺”的做法是当进程申请不到新资源时释放已占有的资源“破坏循环等待”的做法是给所有资源编号要求进程必须按编号递增的顺序申请资源。美团这套题中关于死锁的考察比较直接属于送分题级别。但容易踩坑的地方在于选择题里所给的场景可能同时包含多个必要条件的分析。我的建议是遇到这类题时先在草稿纸上把四个必要条件列出来再逐个对照场景进行排除而不是凭感觉判断。5. 做题时的常见错误与时间分配5.1 容易丢分的三个误区第一是复杂度边界误判。选择题中很多题看着是O(n²)实际是O(nlogn)或者是看着像O(nlogn)实际是O(n)。关键要看循环变量的步进方式以及递归的分支数量。建议做完每道复杂度题后都花10秒钟口头复核一次一层循环走几次两层循环嵌套时内层是否依赖外层变量。第二是算法题边界条件遗漏。手写代码时很多候选人喜欢直接进入主题忽略了对空链表、空数组、单元素数组等特殊输入的判断。比如链表反转如果head为空或只有一个节点直接返回head即可但如果不加这个判断代码会运行出错。实际上边界条件的处理是面试官考察代码完整度的重要指标建议养成“主逻辑写完后先检查边界条件再整理代码格式”的习惯。第三是时间分配失衡。有些候选人死磕一道编程题导致前面的选择题没时间做。一套笔试通常只有90分钟如果前面客观题遇到了计算量较大的题建议先标记跳过去等后面所有题都完成一遍后再回头攻克。通常编程题的分数权重更高如果为了两道选择题的5分丢掉一道20分的编程题得不偿失。5.2 90分钟如何分配答题节奏我根据自己的刷题经验习惯用“10分钟预热、60分钟主攻、20分钟复查”的节奏来做整套笔试题。前10分钟浏览一遍所有题目在草稿纸上记下哪些题是自己一眼就能看穿的“速答题”哪些是计算量大的“思考题”哪些是编程题。这样做能在心理上建立一个答题优先级。中间60分钟先做速答题再啃思考题最后集中精力写编程题。思考题一般每道控制在5到7分钟编程题每题控制在15到20分钟。编程题的代码不要求一次写对但必须写出一版结构完整、能体现核心思路的代码。即使边界条件判断不清楚也要先把主流程写出来给阅卷者一个“这个候选人知道怎么做”的印象。最后20分钟回头检查选择题中的计算题。重点检查时间复杂度、哈希冲突、TCP状态等容易算错的题目。编程题则检查代码是否有明显的语法错误、循环边界是否漏了等号、指针操作是否安全。这里有一个我踩过的坑刚开始刷题时我总喜欢先做编程题再做选择题理由是编程题分值大。但后来发现遇到难题时容易死磕导致后续选择题没有余量时间思考很多本来可以做对的题也选错了。后来我调整为先扫全卷再做分题型攻坚正确率稳定提升了20%左右。6. 刷完这套题后的复盘清单做完并订正完一套题不是合上卷子就结束了。真正的长进来自于复盘。我给自己定了一个复盘清单刷完任何一套笔试题也会按照这个流程走一遍。第一步统计错题分布。把错题按模块归类记录是哪一类知识点的错。如果发现操作系统的错题占比最高说明这一块的知识体系需要回炉再造。建议找出教材或课程对应的章节重新过一遍概念再做10到20道专项练习题。第二步对每道错题做“错因分析”。我一般会标注为四类概念不清晰、计算错误、思路方向错、边界条件遗漏。概念不清晰和思路方向错属于知识盲区需要补课计算错误和边界条件遗漏属于考试技巧问题需要在练习中刻意强化。第三步把编程题重写一遍。不看答案不看自己之前的代码把题目标注出来第二天再重新默写一遍。对就是默写。写完后对比自己的旧代码看看有没有比之前更简洁的写法。这个过程能强化算法题的肌肉记忆让我在真实笔试时不用重新推导。第四步寻找同类题做变体训练。比如链表反转练完后可以主动找环形链表、K个一组反转链表、回文链表这些变体来做。目的是举一反三把单一题目的解法迁移为同类问题的解题思路。这套“美团2016研发工程师笔试题(二)”虽然年份有些久远但它的考点精准覆盖了计算机基础的核心骨架。如果你现在正处于笔试准备阶段我真心建议你以这套题为起点先摸清自己的水平再带着问题去专项提升。刷题本身不是目的构建一套扎实的计算机基础知识体系才是这也是无论行业怎么变化都不会过时的底层能力。
返回列表