小米2018服务端实习生笔试题深度解析:从TCP到LRU缓存 1. 笔试概览与考点分布1.1 2018年这次笔试考了什么看到小米2018春季实习生服务端开发工程师笔试题这个标题我相信很多准备跳槽或者找实习的同学会点进来。毕竟小米的笔试在互联网公司里面算是有代表性的——难度适中、覆盖面广、偏向基础原理不像某些大厂上来就整hard级别的算法题也不会像另外一些公司那样全是偏门八股。2018年这个时间点很有意思那时候移动互联网红利还在服务端开发岗位的竞争还没有现在这么卷但考察的基本盘已经非常明确了。这次笔试整体分为三个部分选择题、编程题和简答题。客观来说选择题覆盖了计算机网络、操作系统、数据库、Linux基础、C/Java语言特性这些经典科目。编程题有两道一道偏算法字符串处理/数据结构另一道偏场景设计线程并发或缓存设计。简答题则是典型的开放问题场景题组合。这套题的参考价值在哪里两点。第一它的题型结构和现在主流互联网公司的实习生笔试高度重合刷一套顶三套。第二小米作为以安卓系统、米聊起家的移动互联网公司它的服务端笔试明显偏向业务落地能力的考察不是纯粹做题而是会把你放在真实场景里看你会不会写代码。1.2 这套题适合谁来参考如果你是正在准备大三暑期实习、或者研二找日常实习的在校生这套题就是你的摸底卷。做完之后你能比较清楚地判断出自己的短板在哪个领域——是算法思路不够还是TCP/IP协议栈没吃透或者是对数据库索引原理一知半解。我也见过不少工作了三五年的人回头来看这套题想给自己做个基本功体检。说实话很多老开发做这套题未必能拿高分因为平时写业务代码很多基础概念会用但讲不清楚、证明不了。这套题恰好能把这类知识盲区逼出来如果你也有这种感觉那这篇文章正好帮你系统梳理一遍。2. 算法编程题深度拆解2.1 典型的字符串处理题找出最长无重复子串先从编程题说起。这类题在2018年前后的笔试里几乎必出小米也不例外的原因很简单——字符串处理覆盖了哈希表、滑动窗口、动态规划三个核心考点一道题就能考察出候选人的基础编码能力。题目形式大致是给定一个字符串找出其中不含有重复字符的最长子串的长度。比如输入abcabcbb答案应该是3abc输入bbbbb答案是1。最直接的暴力解法就是枚举所有子串检查每个子串是否有重复字符时间复杂度O(n^3)面试官看到这种解法基本就把你划入基础不扎实那一类了。你会做这道题不代表什么人人都会但你能不能在短时间内给出最优解并且把边界条件考虑干净这才是区分度所在。我直接给出滑动窗口的标准解法顺便把复杂度讲清楚方便你面试的时候直接用#include iostream #include string #include unordered_set using namespace std; int lengthOfLongestSubstring(string s) { unordered_setchar window; int left 0, right 0; int maxLen 0; while (right s.size()) { if (window.find(s[right]) window.end()) { window.insert(s[right]); right; maxLen max(maxLen, right - left); } else { window.erase(s[left]); left; } } return maxLen; }这段代码的核心逻辑是维护一个窗口窗口内的字符都是不重复的。右指针负责扩展窗口左指针负责收缩。当遇到重复字符时右指针不动左指针不断右移直到窗口内不再有重复字符。每次移动指针后更新最大长度。时间复杂度O(n)空间复杂度O(字符集大小)在笔试场景下这是最优解。我当时实际做这道题的时候犯过一个低级错误——忘记处理空字符串的边界情况导致程序直接越界崩溃。所以提醒大家写完代码之后第一件事不是得意而是自己列举几个特殊输入跑一遍包括空串、单字符、全重复字符、全不重复字符。这个习惯养成了笔试的通过率至少提高两成。2.2 并发场景设计题实现一个带过期时间的LRU缓存第二道编程题我印象更深因为它直接对应服务端开发的高频场景——缓存设计。题目要求实现一个LRULeast Recently Used缓存支持get和put操作并且每个key可以设置过期时间。为什么考这个因为服务端开发离不开缓存。从Redis到本地缓存再到CDNLRU淘汰策略无处不在。小米的业务场景里用户信息缓存、商品详情缓存、验证码缓存全都要用类似的东西。所以这道题考的不是你会不会背LRU实现而是你有没有真正理解过缓存系统的核心矛盾访问速度和内存容量之间的取舍。我给出的实现思路是这样的用哈希表双向链表实现O(1)复杂度的get和put。哈希表负责快速定位节点双向链表负责维护访问顺序。每次get一个key就把对应节点移到链表头部每次put一个新key就检查容量如果满了就淘汰链表尾部的节点。至于过期时间有两种方案。方案一是惰性删除——只在get的时候检查这个key是否已经过期过期就删除并返回不存在。方案二是定时清理——启动一个后台线程定期扫描并删除过期key。笔试场景下方案一就足够因为代码量少、逻辑清晰而且面试官最想看到的是LRU核心逻辑不是并发调度。#include iostream #include unordered_map #include list #include utility using namespace std; class LRUCache { public: LRUCache(int capacity) : cap(capacity) {} int get(int key) { auto it mp.find(key); if (it mp.end()) return -1; if (time(NULL) - it-second.second expireTime) { lst.erase(it-second.first); mp.erase(it); return -1; } lst.splice(lst.begin(), lst, it-second.first); return it-second.first-second; } void put(int key, int value) { auto it mp.find(key); if (it ! mp.end()) { lst.erase(it-second.first); mp.erase(it); } lst.push_front({key, value}); mp[key] {lst.begin(), time(NULL)}; if (mp.size() cap) { int delKey lst.back().first; lst.pop_back(); mp.erase(delKey); } } private: int cap; int expireTime 5; listpairint, int lst; unordered_mapint, pairlistpairint,int::iterator, time_t mp; };这道题我在实际面试复盘里见过很多同学翻车主要翻在三个地方。第一get操作之后忘记更新访问顺序——这会让LRU变成FIFO直接逻辑错误。第二put重复key时没有先删除旧节点导致链表中出现重复节点。第三容量为1的边缘情况没有单独考虑。做完这道题之后我建议你再思考一个问题如果这个缓存是多线程访问的你怎么保证线程安全加全局锁最简单但性能差用读写锁相对好一些sharded锁或者无锁数据结构则更进阶。面试官非常喜欢顺着这个方向追问你如果能主动展开说几句印象分会高不少。2.3 一道关于并发交替打印的经典题除了上面两道题那年笔试还有一道并发编程题让我印象深刻——要求用两个线程交替打印奇数和偶数一个线程打印1、3、5另一个线程打印2、4、6直到100。这题考察的是线程同步的基本功本质上考的是你对锁、条件变量或者信号量的理解。很多同学在IDE里写多线程代码很熟练一到笔试白板写代码就卡壳就是因为对线程同步原语的理解停留在会用API而不是理解机制。我的解法是用互斥锁条件变量#include iostream #include thread #include mutex #include condition_variable using namespace std; mutex mtx; condition_variable cv; int number 1; const int MAX 100; void printOdd() { while (true) { unique_lockmutex lock(mtx); cv.wait(lock, []{ return number % 2 1 || number MAX; }); if (number MAX) { cv.notify_all(); break; } cout Odd: number endl; number; cv.notify_all(); } } void printEven() { while (true) { unique_lockmutex lock(mtx); cv.wait(lock, []{ return number % 2 0 || number MAX; }); if (number MAX) { cv.notify_all(); break; } cout Even: number endl; number; cv.notify_all(); } }这里有个关键点wait必须放在一个循环里不能只用if判断条件。原因是虚假唤醒spurious wakeup的存在——一个线程被唤醒不代表条件一定成立可能需要再次等待。如果用if条件不满足时也会往下执行打印出错误的结果。这个问题在真实面试里被问到过很多次属于多线程编程中的魔鬼细节。3. 计算机基础网络与操作系统的核心考点3.1 TCP三次握手与四次挥手考的不是表象是本质选择题和简答题部分网络绝对是重头戏。尤其是TCP协议几乎年年必考。那年的选择题里有一道是TCP建立连接为什么是三次握手两次行不行很多人背过标准答案防止已失效的连接请求报文突然又传到服务端产生错误。但面试官如果继续追问那你给我具体描述一下这个错误是怎么发生的很多人就卡住了。我来说清楚。假设只有两次握手客户端发送了一个SYN报文因为网络拥堵这个报文在链路中滞留了很久。客户端等不到服务器的确认于是超时重传了一个新的SYN这次连接建立成功数据传输完成后关闭。但这个时候第一个滞留的SYN才到达服务器。服务器收到这个迟到的SYN认为客户端想建立新连接于是返回SYNACK。此时在两次握手的情况下连接就算建立了——但客户端根本不想建立这个连接于是服务器白白分配了资源这些资源在超时释放之前就被浪费了。三次握手怎么解决这个问题如果服务器收到了迟到的SYN并返回SYNACK客户端发现这个连接不是自己发起的就不会再发送ACK确认。服务器收不到ACK就不会建立连接资源也就不会分配。类似的考点还有TIME_WAIT状态为什么需要存在答案是保证最后一次ACK能到达对端同时让旧连接的所有报文在网络上全部消失防止干扰新连接。那TIME_WAIT的时间为什么是2MSL因为MSLMaximum Segment Lifetime是报文在网络中存活的最长时间一个报文发出后最多经过2MSL才能被确认消失。这个知识点选择题和简答都可能考属于如果不理解就只能死记硬背理解了就很难忘记的典型。3.2 操作系统进程、线程与死锁的四必要条操作系统部分小米那年考了进程和线程的区别以及死锁产生的四个必要条件。进程和线程的区别常规答法是进程是资源分配的最小单位线程是CPU调度的最小单位。但这个答案太单薄。我建议从三个维度展开资源占用、通信方式、切换开销。资源占用上进程拥有独立的地址空间文件描述符表、信号处理器都是独立的线程共享进程的地址空间和资源每个线程只拥有独立的栈和寄存器上下文。通信方式上进程间通信需要用管道、消息队列、共享内存、信号量这些IPC机制而线程间通信只需要读写共享变量。切换开销上进程切换涉及地址空间的切换开销大线程切换不涉及地址空间切换开销小得多。死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待。这四个条件缺一不可所以死锁的预防策略就是打破其中任意一个条件。但要注意这四件事在操作系统的教材里和很多面试答案里写的都一样真正的加分项是你能否举出一个具体的死锁例子。比如经典的哲学家就餐问题或者更务实的例子线程A持有数据库连接1等待连接2线程B持有连接2等待连接1。这样两个线程就互相卡死了。3.3 数据库索引为什么用B树而不是红黑树数据库的考察点非常集中索引、事务隔离级别、SQL优化。小米那年考了B树和红黑树的区别这算是一个高频考点。核心原因有三点。第一B树是多路平衡搜索树出度大、树高小。InnoDB的页大小默认16KB一个节点可以存储几百个键值三层B树就能存储上千万条记录也就是意味着查询最多只需三次磁盘IO。红黑树是二叉的树高大约是log2(n)数据量大了之后每次查询的磁盘IO次数会显著增多。第二B树的所有数据都存在叶子节点并且叶子节点之间通过链表连接非常适合范围查询和全表扫描。你写一条BETWEEN查询找到起始位置后顺着链表往后走就行。红黑树是二叉搜索树中序遍历才能得到有序序列范围查询需要回溯父节点效率低很多。第三B树的中间节点不存数据只存索引同样的内存可以缓存更多索引节点命中率更高。这一点在实际运行中非常关键InnoDB的缓冲池就那么点大能多缓存一层索引就能省不少磁盘IO。4. 语言基础与Linux实战能力考察4.1 C和Java的经典陷阱题小米的笔试明确区分了C和Java两种语言方向报名的时候可以选但不管选哪个都有一些看似简单实则全是坑的语言细节题。C方向那年考了虚函数和虚函数表的原理。问你一个类有虚函数和无虚函数它在内存中的大小有什么区别这个问题能非常有效地筛选出真正理解C对象模型的候选人。有虚函数的类对象内部会多一个指向虚函数表的指针vptr在64位系统上这个指针占8字节。注意这个vptr是在对象内部而不是对象外部单独分配。还有构造函数为什么不能是虚函数因为vptr是在构造函数执行时才被初始化的如果构造时调用虚函数虚函数表还没建立根本无法实现动态绑定。Java方向考了HashMap在JDK 7和JDK 8的区别。这个现在已经是普及型考题了但在2018年还是很有区分度的。JDK 8之前HashMap底层是数组链表有哈希冲突的时候在链表尾部插入最坏情况下get操作退化成O(n)。JDK 8改成了数组链表红黑树链表长度超过8、且数组长度大于等于64时链表会转为红黑树把最坏时间复杂度降到了O(log n)。此外JDK 8中还把头插法改成了尾插法解决了多线程环境下put操作导致死循环的问题。如果你能连这个死循环的具体成因都讲清楚面试官基本就会点头了。4.2 Linux排查问题考的是你是不是真用过笔试题里出现了一类让我当时有点意外的题目——给出一个线上接口变慢的问题场景让你用Linux命令去排查。这类实操向的题目能刷掉一批只在教程里看过Linux的候选人。考察的命令无非是top、ps、free、df、netstat、lsof。但注意不是让你背命令的参数而是给你一个具体场景让你选择合适的命令。比如说接口CPU占用率过高你该怎么查我的习惯是四步走。第一步top命令看全局哪个进程的CPU占用率最高。第二步top -H -p 看这个进程内哪些线程消耗CPU最严重。第三步printf %x\n 把线程ID转成十六进制然后用jstackJava或者gdb attachC看这个线程当前在干什么。第四步结合代码定位是不是某个循环逻辑有问题或者是不是GC过于频繁导致CPU飙高。如果是在笔试里回答这种题目不需要你写出完整的命令链但至少要能让面试官感觉到你真的在线上排查过问题。比如你提到top按P键按CPU排序、按M键按内存排序提到netstat -anp可以看端口和进程的对应关系提到lsof -p 可以列出进程打开的所有文件——这些细节就是区分用过和听说过的分界线。5. 避坑指南与备考策略5.1 我做这套题时踩过的三个大坑第一个坑是时间分配严重失衡。那年笔试的选择题里面有几道网络和操作系统的题目我因为想每道题都拿满分在前面的选择题上花了太多时间导致后面编程题只剩不到四十分钟。等到编程题写完之后简答题基本是草草作答。复盘的时候我意识到笔试题的分数分布通常是大题占比更高前面的选择题即使错了三四道只要大题答得好总分就不会差。后来我给自己定了个铁规矩先花五分钟扫一眼全卷做完编程题之后再回头抠选择题保证拿分大头先到手。第二个坑是C编程题忘记处理内存和边界条件。有些题目看起来简单真正下笔写的时候才发现自己连循环边界条件都理不清。吃了亏之后我开始刻意训练自己把代码写得防御性更强就是每次进入函数第一件事就是检查输入参数的合法性每次循环都明确考虑空集和单元素集的情况。第三个坑是简答题答得太口语化。笔试的简答题不是面试你没法用你懂的这种表情和语气补充。改卷人只能通过文字理解你的思路口语化的答案看起来就是不专业、不确定。我后来学到一个技巧简答题按先说结论再给原因最后举例的结构写这样哪怕你的结论不够准确至少思路是清晰的能拿到过程分。5.2 针对服务端实习生的战前突击清单如果你现在正在准备类似的笔试我建议你按下面的清单做战前突击按优先级排序第一优先级是数组、链表、栈、队列、哈希表、二叉树这六种基础数据结构的实现和典型算法LeetCode上Easy和Medium难度的题目各刷30道就够用。第二优先级是计算机网络里的TCP三次握手/四次挥手、HTTP/HTTPS的区别、DNS解析过程操作系统里的进程线程区别、死锁、虚拟内存、页面置换算法。第三优先级是你所报语言的核心特性——C就重点看虚函数、智能指针、STL容器的底层原理Java就重点看JVM内存模型、集合类、并发工具包。对于数据库至少要掌握索引原理、B树为什么适合做索引、事务的ACID和隔离级别、慢查询的排查思路。Redis相关的内容虽然在笔试里不一定直接考但面试环节被问到的概率极高建议提前准备缓存穿透、缓存击穿、缓存雪崩这三个经典问题。5.3 笔试之后的面试环节怎么衔接笔试只是第一关小米的面试通常有三到四轮其中至少一轮是技术面。笔试中你会做错或者做不出来的题目很有可能被面试官拿出来复盘——他不想听你说我忘了而是想看你在面试的高压环境下能不能补上思路。举个例子笔试里你没做出来LRU那道题面试官可能会换一道实现一个LFU或者讲讲Redis的LRU近似实现来考察你。如果他发现你回去以后认真把LRU的各种变体都练习过会给你加印象分。反过来如果你笔试完了就再也不看题目面试官问起来你还是一脸茫然那基本就凉了。还有一个容易被忽视的细节笔试时你的解题代码风格、变量命名习惯、注释质量其实都会被面试官看到。服务端开发是个协作密集型的岗位代码可读性非常重要。我在笔试的时候习惯在关键步骤写一行注释解释这步的逻辑这样即使代码写错了面试官也能看出我的思路是正确的。6. 从2018年看今天一套旧题背后的行业信号6.1 题目为什么这么出回头看2018年这套题我有一个很强烈的感受小米作为一家靠硬件互联网服务两条腿走路的公司笔试考察的服务端开发能力特别看重落地性和系统性。它不会像学术型公司那样考一堆算法竞赛级别的难题也不会像部分外包公司那样只考JSP和SSH框架的用法。它考的是你作为一个新人能不能在入职后最短时间内完成接到需求 - 设计模块 - 写代码 - 应对并发 - 排查线上问题这个完整闭环。为什么网络和操作系统占比这么高因为服务端开发的本质就是处理大规模并发请求下的资源调度与数据一致性问题。你用任何语言、任何框架最终都要落到CPU怎么调度、内存怎么管理、网络报文怎么传输这些底层机制上。框架隔几年就换一轮底层原理二十年没怎么变过。所以面试官考的不是你会不会用Spring Boot而是你懂不懂HTTP协议、懂不懂线程池。6.2 现在准备笔试的人应该怎么借鉴很多同学爱刷最新真题总担心2018年的题太旧了没有参考价值。但从我实际辅导过的应届生情况来看现在各家公司的服务端笔试核心考点其实和2018年没有本质区别。技术面考察的底层原理、算法编码能力、代码风格、问题排查思路决定因素一直没变。真正变化的趋势有两个。第一越来越多的公司开始加入系统设计类题目比如让你设计一个短网址服务、设计一个秒杀系统。这类题在2018年的实习生笔试中比较少见但现在的出镜率已经很高了。第二对容器化和云原生知识的考察比重明显上升Docker和Kubernetes相关的基础题偶尔会出现在笔试题里。但这不意味着你可以不刷基础题。恰恰相反系统设计题的本质是在基础原理之上做组合——你要懂数据库事务才能设计秒杀扣减库存要懂消息队列才能设计异步削峰要懂缓存才能设计热点数据访问。地基不牢设计就是空中楼阁。我个人带过不少转行的朋友和在校生他们最容易犯的误区就是先学框架、后补基础总觉得Spring Boot能跑起来就是会服务端开发了。等到真正面试的时候才发现框架API是很好查的而TCP协议的状态变迁、B树的分裂合并、线程池的参数调优这些才是真正拉开差距的东西。小米这套2018年的笔试题恰好就是一面镜子照出来的不只是知识储备还有你对服务端开发这件事的理解深度。越早看清这一点准备的方向就越明确。这套题做完不是把它扔在一边而是每一道错题都值得你追问一句这个考点对应的真实工程场景是什么它为什么重要想清楚这两个问题你从这套题里得到的收获会比单纯刷十套新题更多。