ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

2020美团后台开发笔试题库解析:高频考点与真题复盘

2020美团后台开发笔试题库解析:高频考点与真题复盘 2020年那阵子美团校招后台开发方向的笔试在圈内讨论度一直不低。和很多大厂动辄四道算法题“一锤定音”的风格不同美团的卷子更偏向“基础广度的快速扫描”选择题覆盖操作系统、计算机网络、数据库、Java/C语言特性后面再跟两道左右在线编程题。整体感觉就是它不指望你在两个小时里写出一个惊艳的编译器但要求你在“后台开发工程师应该知道的常识”这件事上不掉链子。这篇文章我按当年的题目风格做了一个系统拆解把高频考点、典型真题的推荐解法、以及我在反复看这套题时总结出的底层逻辑都过一遍。无论你是正准备投美团后台开发岗还是想用一套真题来检验自己的计算机基础这篇都值得认真看完。1. 2020年美团后台开发笔试卷面结构解析先说卷面。美团校招的笔试通常是牛客网在线笔试系统后台开发方向的卷子题量大概在20道选择题加2道编程题左右编程题有时候是3道但分值占比会调整。整个考试时间一般是90到120分钟选择题和编程题在同一场里完成时间分配很考验人。这套题有三个很明显的特征。第一选择题覆盖面极广。操作系统、网络、数据库、Java并发、JVM、Linux常用命令、设计模式、数据结构复杂度分析几乎每个方向都点到。广度远超深度几乎没有偏题怪题都是“你应该知道”的知识点。这意味着你对基础知识的记忆留存率决定了选择题的得分上限。第二编程题难度呈梯度分布。第一题通常是纯数据结构题难度在LeetCode中等偏下水平只要你刷过题基本都能写出来。第二题就开始贴近业务场景了可能是字符串处理、模拟某种逻辑、或者带一点贪心和动态规划的味道光会背模板不行得能快速把题目翻译成代码。第三这套题对“快”的要求非常高。选择题每题只有一分多钟的思考时间编程题要在剩余时间里完成读题、设计、编码、调试。很多同学挂在编程题上不是因为不会而是前面选择题磨太久后面只剩二十分钟心态直接崩了。我当年给学弟学妹的建议是选择题遇到一眼不会的先凭直觉选一个并标记不要恋战把时间留给编程题。因为编程题一题的分值往往顶得上五道选择题性价比更高。另外提醒一句美团的笔试系统支持本地编译器调试但提交后以系统判题结果为准。不要试图在本地环境里依赖IDE的语法提示平时练习就要习惯在纯文本环境下写代码。2. 算法与数据结构类题目的高频考题与解题思路美团后台开发的算法题非常偏爱链表、字符串、哈希表和场景模拟这四类。因为它考察的不是“你是否掌握某个高级算法”而是“在限定时间内能否用基础数据结构干净地解决问题”。当年卷子里出现过一道单链表反转这算是面试笔试里的“见面礼”。题目很简单给定一个单链表返回反转后的链表。很多人觉得这题一眼就会但真正动手写往往把迭代的指针绕晕。public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next prev; prev curr; curr next; } return prev; }时间复杂度O(n)空间复杂度O(1)。核心思路就一句话把当前节点的next指向前一个节点然后三个引用整体后移。这里有一个细节很多人在循环里忘了先保存curr.next一旦执行了curr.next prev原来的后继节点就丢了链表直接断掉。所以每次循环的第一步一定是保存后继节点。除了迭代法如果面试官追问可以用递归再写一版。public ListNode reverseList(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseList(head.next); head.next.next head; head.next null; return newHead; }递归写法理解起来更抽象但代码更简洁。它的精髓是“先反转后面的子链表再把当前节点接到末尾”。另一道我非常眼熟的题是“大数相加”。美团喜欢考这种题是因为真实业务里经常遇到数值超出long范围的场景比如订单号、用户ID的拼接计算。题目通常会以字符串形式给两个非负整数要求返回它们相加的结果字符串不能直接用BigInteger。思路要用小学加法的“竖式”来理解从两个字符串的最低位开始逐位相加维护一个进位变量carry把每一位的加和结果模10拼入结果串最后反转。public String addStrings(String num1, String num2) { int i num1.length() - 1; int j num2.length() - 1; int carry 0; StringBuilder sb new StringBuilder(); while (i 0 || j 0 || carry ! 0) { int x i 0 ? num1.charAt(i) - 0 : 0; int y j 0 ? num2.charAt(j) - 0 : 0; int sum x y carry; carry sum / 10; sb.append(sum % 10); i--; j--; } return sb.reverse().toString(); }这题最隐蔽的坑有两个。第一个是char转int时必须减0直接强转会得到ASCII码值这是新手常犯错误。第二个是结束条件一定是i 0 || j 0 || carry ! 0最后一位计算完如果还有进位必须再补一位否则结果会少一个最高位。还有一道长度中等偏上的题是LRU缓存机制当时我猜这题是压轴题之一。实现一个固定容量的LRU缓存支持get和put操作get和put的平均时间复杂度都要求O(1)。LRU的经典解法是“哈希表双向链表”组合哈希表负责O(1)的查找双向链表负责O(1)的增删和移动。class LRUCache { class Node { int key; int value; Node prev; Node next; public Node(int key, int value) { this.key key; this.value value; } } private int capacity; private HashMapInteger, Node map new HashMap(); private Node head new Node(-1, -1); private Node tail new Node(-1, -1); public LRUCache(int capacity) { this.capacity capacity; head.next tail; tail.prev head; } public int get(int key) { Node node map.get(key); if (node null) { return -1; } moveToHead(node); return node.value; } public void put(int key, int value) { if (map.containsKey(key)) { Node node map.get(key); node.value value; moveToHead(node); } else { Node node new Node(key, value); map.put(key, node); addToHead(node); if (map.size() capacity) { Node removed removeTail(); map.remove(removed.key); } } } private void moveToHead(Node node) { removeNode(node); addToHead(node); } private void removeNode(Node node) { node.prev.next node.next; node.next.prev node.prev; } private void addToHead(Node node) { node.next head.next; node.prev head; head.next.prev node; head.next node; } private Node removeTail() { Node node tail.prev; removeNode(node); return node; } }写这种题最难受的环节是双向链表增删时指针顺序搞乱。比如addToHead方法里如果先把node.next指向head.next再把head.next指向node中间如果忘了调整原来后继节点的prev指针链表就穿串了。我的习惯是先处理新节点和它的后继再处理head和node的关系最后处理后继指向node这样思路更顺。这三个类型的题共同指向一个结论美团后台开发笔试里的算法题不像竞赛那样偏难怪但特别看重代码的准确性、边界敏感性、以及对数据结构本质的理解。刷题阶段不要只追求AC要逼自己把链表的指针变化、哈希表的装载因子、递归的栈帧过程讲清楚。3. 操作系统与计算机网络后台开发的底层基础题盘点我第一次做这套选择题时最大的感受是题不难但特别细。操作系统和网络在选择题里占比大概三分之一到四分之一属于丢分重灾区。不是知识点不会而是选项设置得很有迷惑性。进程与线程的区别几乎是必考。有一题这样描述关于进程和线程下列说法正确的是。选项里常见的错误说法包括“线程是资源分配的基本单位”“进程之间不能通信”“同一个进程的不同线程共享所有内存区域”。正确答案是“线程是CPU调度的基本单位进程是资源分配的基本单位”。后台开发这个岗位每天都会接触多线程编程如果把这两者混淆系统设计题会直接崩掉。另一个常见考点是死锁的四个必要条件互斥、请求并保持、不可剥夺、循环等待。题型通常是给一个场景问破坏了哪个条件可以预防死锁。比如“资源一次性分配”破坏的是“请求并保持”“可通过强制回收资源”破坏的是“不可剥夺”“资源有序分配法”破坏的是“循环等待”。2020年的卷子里有个选项很有趣描述是“将互斥资源改为共享资源”这显然不现实互斥性往往是资源本身的性质决定的只能降低互斥范围无法强行消除。TCP连接管理也是高频考点。三次握手、四次挥手、TIME_WAIT状态存在的意义这些几乎是后台开发笔试的“规定动作”。关于TIME_WAIT有一道题问的是“客户端主动关闭连接后进入TIME_WAIT状态为什么要等待2MSL”。大多数人的回答是“确保最后一个ACK到达对端”这没错但只说到了第一层。更完整的解释是如果客户端最后一次ACK丢失服务端会重发FIN客户端需要留出时间处理这个重发的FIN否则服务端会一直得不到确认同时让本连接产生的所有报文段在网络中消失防止端口复用时旧连接的报文干扰新连接。在美团后台开发的语境里TIME_WAIT和大量短连接的性能关系也值得思考。高并发场景下client主动关闭连接会堆积大量TIME_WAIT套接字那是否要开启tcp_tw_reuse这就是笔试之外真正的工作场景延伸了。虚拟内存与页面置换算法也常出现。有一道题问LRU页面置换算法在一个3页物理块的进程中处理某个页面引用串时会发生多少次缺页中断。这类题没有技巧你得真的一步一步在草稿纸上模拟。题目通常是给定页面引用串 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5内存块数3使用LRU算法统计缺页次数。模拟过程如下访问1缺页内存{1}访问2缺页内存{1,2}访问3缺页内存{1,2,3}访问4缺页淘汰1内存{2,3,4}访问1缺页淘汰2内存{3,4,1}访问2缺页淘汰3内存{4,1,2}访问5缺页淘汰4内存{1,2,5}访问1命中内存{2,5,1}访问2命中内存{5,1,2}访问3缺页淘汰5内存{1,2,3}访问4缺页淘汰1内存{2,3,4}访问5缺页淘汰2内存{3,4,5}总共10次缺页。这种题特别容易在“命中时是否需要更新访问顺序”上出错LRU要求每次命中都要把该页移到最前这是它的核心机制。选择题里还出现过select、poll、epoll的对比。美团那么大的后台系统高并发IO模型是基础。题目问在连接数较多但活跃连接占比很低的情况下哪种IO多路复用模型效率最高。答案是epoll因为epoll通过事件驱动和回调机制只处理活跃连接不会像select和poll那样每次调用都遍历全部文件描述符集合。如果把select、poll、epoll的区别展开可以从三个角度记忆文件描述符数量限制、IO效率、消息传递方式。select受FD_SETSIZE限制默认1024poll没有上限epoll没有上限而且采用mmap加速内核与用户空间的消息传递。这些点单独看都简单但放到选择题里组合起来就很容易看错。4. 数据库与系统设计场景题从业务出发的考察逻辑美团毕竟是做本地生活服务的数据和业务联系紧密数据库在选择题里占的比例相当高偶尔还会有一道和系统设计沾边的场景题。SQL类的题目一般是给两张表要求写出查询结果。考察重点包括GROUP BY和聚合函数、JOIN的类型差异、WHERE与HAVING的执行顺序、索引命中情况。有一道印象深刻的题有一张订单表ordersid, user_id, amount, create_time要求统计每个用户的总订单金额并筛选出总金额大于1000的用户按金额降序输出。正确SQL在很多教材里都有但美团考的难点在于“WHERE和HAVING谁可以过滤聚合结果”。SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id HAVING SUM(amount) 1000 ORDER BY total_amount DESC;这里不能用WHERE SUM(amount) 1000因为WHERE是在分组前执行的而聚合函数SUM是在分组时计算的所以过滤分组后的聚合结果只能用HAVING。执行顺序上是先FROM再WHERE再GROUP BY再HAVING再SELECT最后ORDER BY。这张执行顺序图建议刻在脑子里不光笔试要用实际排查SQL问题也有用。索引相关的题也很典型。有一道选择题是“在联合索引(a, b, c)下哪些查询可以利用该索引”。答案是只查询a、或者a和b、或者a和b和c的查询能利用索引直接跳a去查b或c的查询无法利用联合索引的最左前缀原则。这是因为联合索引的B树节点按照字段顺序依次排序只有遵循了最左前缀查询条件才能与索引的有序性匹配。系统设计题在2020年的卷子里不是让你写完整方案而是以场景题的形式出现。比如有一道题描述了外卖订单表随着业务增长数据量达到千万级查询延迟上升问以下哪种处理方式最有效。选项包括给所有字段建索引、分库分表、增加查询超时时间、用存储过程。答案是分库分表。这里我想多说一句真正业务里的分库分表远比选择题复杂。美团外卖订单量那个级别不可能简单按user_id取模就完事。需要考虑数据分布均匀性、跨分片查询代价、扩容数据迁移成本、全局唯一ID生成等问题。笔试里单选分库分表是拿分的但你如果能在面试环节主动聊出“取模分片的扩容问题”和“用一致性哈希缓解扩容抖动”那就是明显的加分项。还有一道和缓存相关的情境题。题目描述某接口读多写少数据库压力大引入Redis缓存后当数据库数据更新时以下哪种缓存更新策略能尽量避免数据不一致。正确做法是“先更新数据库再删除缓存”。这个操作顺序在缓存领域被反复讨论过。先删缓存再更新数据库缓存删除后到数据库更新完成前如果有并发读请求旧数据会被重新加载进缓存造成长时间不一致而先更新数据库再删缓存即使这次删除失败等待缓存过期也能最终一致。美团后台开发的场景题本质上在考察你有没有从业务反推技术选型的能力。它不是考你一个孤立的知识点而是考你在真实系统里做取舍的能力。这种思维在刷题阶段容易被忽略但它是区分“做题家”和“工程师”的关键。5. 编程实践题从真题出发的完整代码实现与细节优化编程题部分我挑一道我在整理这套真题时觉得最有代表性的题目展开讲。它当年在牛客网上的通过率不到20%题目描述很长但拆解之后本质是模拟题加一点排序逻辑。题目大意是这样美团配送系统里有若干骑手每个骑手有配送时间段[start, end]现在同时来了发单请求要求判断一个订单能否被指派给骑手。给出多个骑手的时间段再给一个订单需要的时间窗口判断是否存在至少一个骑手的时间段能完整覆盖订单时间段。骑手的时间段可能重叠但一个骑手同时只能跑一个订单。输出能接单的骑手数量。第一版解法很简单直接遍历每个骑手区间判断订单的start是否大于等于骑手start且订单的end是否小于等于骑手end满足则计数加一。但题目有一个关键限制订单量极大达到10的5次方级如果每笔订单都遍历全部骑手整体复杂度就是O(N*M)铁定超时。优化的核心思路是先对骑手时间段排序再用一种“滑动窗口”的思维去处理。如果把骑手的时间段看作一个个区间按照开始时间升序排列那么对于任意一个订单只需要找到第一个开始时间不晚于订单开始时间的骑手然后在这些骑手中检查最大可用结束时间是否不小于订单结束时间。这里用一个贪心策略维护一个优先队列按照开始时间从小到大把骑手加入另一维度按照结束时间维护一个最大堆表示当前可用骑手中最晚的结束时间。每来一个订单把所有开始时间不晚于订单start的骑手入堆然后把堆中结束时间小于订单start的骑手弹出因为订单开始前这些骑手就结束了不满足覆盖条件接着看堆顶骑手的结束时间是否大于等于订单end如果是说明至少有骑手能接单。import java.util.*; public class RiderAssignment { static class Interval { int start; int end; public Interval(int start, int end) { this.start start; this.end end; } } public static ListInteger solve(ListInterval riders, ListInterval orders) { // 骑手按开始时间升序排序 riders.sort((a, b) - a.start b.start ? a.end - b.end : a.start - b.start); // 订单也按开始时间升序排序但输出要回到原顺序 int m orders.size(); Integer[] idx new Integer[m]; for (int i 0; i m; i) idx[i] i; Arrays.sort(idx, (a, b) - orders.get(a).start - orders.get(b).start); ListInteger result new ArrayList(Collections.nCopies(m, 0)); PriorityQueueInterval pq new PriorityQueue((a, b) - b.end - a.end); int riderIndex 0; int n riders.size(); for (int i 0; i m; i) { Interval order orders.get(idx[i]); // 所有开始时间不晚于订单开始的骑手入堆 while (riderIndex n riders.get(riderIndex).start order.start) { pq.offer(riders.get(riderIndex)); riderIndex; } // 弹出结束时间早于订单开始的骑手 while (!pq.isEmpty() pq.peek().end order.start) { pq.poll(); } // 检查堆顶骑手 if (!pq.isEmpty() pq.peek().end order.end) { result.set(idx[i], 1); } } return result; } }代码量不大但有几个细节很容易出错。第一个是排序稳定性。订单排序后索引会乱一定要用一个idx数组记录原位置最后把结果填回原位置。很多人直接把订单对象排序最后结果数组顺序和原输入对不上导致全盘WA。第二个是优先队列存储的是引用还是副本的问题。直接把Interval对象扔进堆里如果后续修改对象字段会影响堆内数据但这里我们入堆之后不会修改它的字段所以没问题。第三个也是最重要的一点为什么堆顶的结束时间一定最大化因为优先队列按结束时间降序排列堆顶就是当前所有可用骑手中结束时间最晚的那一个。如果最晚的骑手都没法覆盖订单的end那其他骑手更不可能。这个“如果最晚都不行其他都不行”的贪心判断是这道题性能优化成立的核心基础。这道题的最坏时间复杂度是O((NM)logN)相比暴力遍历的O(NM)性能提升了几个数量级。笔试里一旦出现大数据范围提示基本就说明必须用这种带排序加优先队列的解法。6. 关于这套笔试真题我的复盘心得与准备建议把美团2020年这套后台开发笔试题完整复盘之后有一个感受非常强烈它不像很多公司的题那样追求“秀肌肉”把竞赛级别的算法题堆上来它更像是在模拟一个后台开发工程师日常面对的问题集。网络、操作系统、数据库、并发、代码实现这些恰恰是每天写业务代码都要触碰的东西。如果你准备投美团这类大厂后台开发岗刷题方向上我有几个具体建议。第一数据结构基础题必须练到“肌肉记忆”。单链表反转、快慢指针找中间节点、两数之和、有效的括号、二叉树层序遍历、LRU、大数相加这些题出现频率极高。我的标准是闭上眼睛把代码背着敲出来调试时间不超过五分钟。不是提倡死记硬背而是这些代码的思维模式已经内化到了“看到题目就能条件反射”的程度。第二操作系统和网络不要裸背八股用“为什么”来串联知识点。比如TCP为什么需要三次握手而不是两次因为要确认双方的接收和发送能力都正常TIME_WAIT为什么是2MSL因为这个时间足够让旧连接的所有报文段消失。理解背后的推理过程后不管题目怎么换变体你都能答对。第三系统设计题即使笔试只考选择题也要当成问答题来准备。美团特别喜欢把业务场景和分布式基础合在一起考。建议把缓存穿透与击穿、消息队列削峰、分库分表策略、分布式锁的实现这几个主题提前整理一遍笔试遇到时能更快判断面试时也能深入聊。第四时间分配永远别倒挂。我见过太多人搞反了优先级选择题冥思苦想编程题草草收场。我的建议是拿到卷子先把所有题目扫一遍看编程题的难度分布心里有个底再回头做选择题。如果编程题比较复杂给编程题留足至少四十五分钟选择题遇到犹豫超过两分钟的题直接标记跳走。最后再说一个心态层面的体会。2020年那批笔试刷下来你会发现所谓“难”往往不是因为题目本身而是因为基础不牢时碰到的一个个在课本上见过、但没真正消化的点链表指针指错了、联合索引最左前缀记反了、LRU模拟漏了“命中更新顺序”。这些点单个拿出来都不可怕可怕的是在两个小时的高压环境下同时爆发。把美团这套题吃透它不仅是一次求职准备更像是对后台开发核心知识体系的一次体检。明确自己的弱项按部就班补齐后面再遇到美团或者其他大厂的笔试题你可以坐下来稳稳地把它写完而不是靠运气蒙对几道题。
返回列表