先说明一点:手头这份标题是“小米2020校招软件开发工程师笔试题一”,并没有附带完整题目原文。所以这篇博文我是按当年小米软件研发岗校招笔试的真实风格,把最常出现的题型结构、典型考点、解题思路和现场踩坑经验做了完整复盘。如果你手上有原题,按这套拆解方法去对,也能直接把每道题的考点和答题框架套进去。
2020年那会,小米校招笔试还是三大块的经典布局:选择题 + 编程题 + 问答题。软件开发工程师岗位整体面向Java/C++方向,笔试时间普遍在90到120分钟,题量不低。我当时拿到这套题的第一感觉是:选择题考得很细,不是那种背概念就能过的题,它非常喜欢把两个容易混淆的知识点放在一起让你分辨;编程题反而不算特别偏难怪,两道题都是“你肯定见过,但一着急就容易写错”的类型。
这篇文章我把这套卷子的核心考点拆成四块:卷面结构、高频选择题考点解析、两道编程题的完整解法、逻辑智力题的通用思路。最后把我自己踩过的坑和带同学复盘时发现的高频失误一起列出来。内容偏实战,目标读者是正在准备软件研发岗校招的同学,也适合想系统复习数据结构、操作系统、网络基础的人。
1. 试卷概况与考察重点
1.1 2020年小米软开笔试题的整体结构
先说卷面结构。2020年小米软件开发工程师的笔试分为几个固定板块:第一部分是单选题,第二部分是多选题,第三部分是编程题,第四部分是问答题/逻辑题。这个结构和很多大厂校招不太一样的是,它保留了问答题,而且问答题里经常混着逻辑智力题,这是小米历年笔试的一个特色。
整套题的时间压力主要来自选择题。我印象很深的是,单选加多选大概有30到40道题,覆盖C++/Java语法、操作系统、计算机网络、数据结构、数据库、Linux常用命令这几个方向。很多同学在这部分耗了太多时间,导致后面编程题只能草草提交。这是一个非常典型的策略失误。
备考时可以先明确:这套卷子的核心筛选逻辑不是“看你背了多少知识点”,而是“看你在有限时间内能不能稳定输出基础能力”。选择题设计了不少干扰项,它考验的是你对知识的精确记忆。比如C++里指针和引用的区别、多线程下volatile的真实作用、TCP三次握手为什么不能减成两次,这些老生常谈的东西它换着花样出。
1.2 为什么这样出题:笔试背后的能力模型
从岗位能力模型来看,小米软开笔试考的东西其实就三类:计算机基础底子、代码实现能力、逻辑拆解能力。选择题对应第一类,编程题对应第二类,问答题和智力题对应第三类。这和实际工作后的要求是匹配的——你写业务代码时,操作系统和网络的知识会在排查问题时候反复用到;你对数据结构的熟练程度,直接决定了代码质量的上下限。
我自己复盘下来,这套卷子给后来人最大的启示是:复习一定要抓住主干。历届同学经常犯的错是去钻偏门,比如研究C++模板元编程、背一大堆Linux冷门命令,结果笔试时发现试卷考的还是那几板斧:static关键字、虚函数、堆和栈、进程线程区别、TCP和UDP对比、排序算法复杂度。主干不牢,偏门再多也没用。
2. 高频选择题考点逐个拆解
2.1 C++/Java基础:static、指针与引用
选择题里C++相关题量最大,其中最常出现的考点是static关键字。它有两个考察层次:第一层是static修饰局部变量时,变量生命周期变成整个程序运行期,且只在第一次执行到声明语句时初始化;第二层是static修饰类成员时,它属于类而不属于某个对象,所有对象共享同一份存储。2020年这版选择题里有一道题特别喜欢挖坑,它把静态局部变量初始化和普通局部变量初始化混在一起,让你判断输出结果。如果你对“静态局部变量只初始化一次”这个结论不够熟悉,很容易算错。
指针和引用的区别也是必考。一句话记法:指针是可以重新指向其他对象的,引用在初始化之后就绑死了;指针可以不初始化,引用必须初始化;指针有多级,引用没有。笔试里它会让你判断哪种写法合法,比如int &ref = 10;这种显然是非法的,因为非常量引用不能绑定字面量。这类题不需要你背概念,只需要你把每一条“规则”还原到实际代码里看是否通过编译。
我建议复习这类考点时,不要只看书,直接把每个结论写成最小可运行代码跑一遍。比如static的初始化时机,用一个包含静态局部变量的函数连续调用三次,打印结果。如此操作一次的效果远好于背十遍教材。
2.2 操作系统与并发:从线程安全到死锁条件
操作系统方向的题在笔试里占比也相当高,尤其是并发相关的内容。考来考去就是这几个点:进程和线程的区别、死锁的四个必要条件、锁的粒度、线程安全的实现方式。2020年这套题就有一道辨析题,大概是给你一段多线程累加代码,问为什么最终结果小于预期值。这里核心考点是i++不是原子操作,它分成了读取、自增、写回三步,多个线程交替执行时会互相覆盖。
死锁那部分,建议把那四个条件背得滚瓜烂熟:互斥、请求并保持、不可剥夺、循环等待。选择题经常会给你一个场景,让你判断破坏了哪个条件后死锁可以避免。比如“允许进程强行抢占其他进程占用的资源”,对应破坏的就是不可剥夺条件。这种题一旦理解到位,基本就是送分。
还有一个高频考点是用户态和内核态的切换。笔试不会直接问“什么是系统调用”,而是给你几个操作,让你选哪些必须切换到内核态。比如文件读写、网络socket通信、内存分配,这些都是。但如果你只是算一个两个整数相加,那纯用户态就完成了。这个区分标准很简单:凡是涉及硬件资源访问或进程间通信的操作,基本都要进内核态。
2.3 计算机网络:TCP三次握手为什么不改成两次
网络部分的选择题以TCP/UDP为主,20年考了一道很经典的辨析:为什么TCP建立连接需要三次握手,两次行不行。答案思路要清晰:三次握手的核心目的不是确认双方都能收发数据,而是让双方都确认“自己发给对方的初始序号已经被对方收到”。如果只握手两次,服务端无法确认客户端是否已经收到自己发来的SYN+ACK,也就无法确定序号同步是否完成。
另外一个常考的是TCP和UDP的区别。笔试喜欢考“下列哪个应用适合用UDP”。记住这几个就够了:视频直播、语音通话、DNS查询场景能忍受少量丢包、追求低延迟,所以用UDP;文件传输、邮件、网页浏览要求数据完整可靠,用TCP。它如果问你“TCP是怎么实现可靠传输的”,你至少要能列出来:序列号、确认应答、超时重传、流量控制、拥塞控制。
网络这块复习性价比非常高。考点高度集中,基本不会超纲。把三次握手四挥手的状态变化图默写一遍,把UDP和TCP差别的表格列出来,笔试选择题丢分就会很少。
2.4 数据结构与算法:数组、链表、栈、队列、二叉树
选择题里数据结构部分的题量占比其实不如C++和操作系统多,但每道题都很有区分度。它不会直接问你“数组和链表的区别”,而是出一个实际场景,让你选合适的数据结构。比如“需要频繁在头部插入删除,但随机访问很少”,那用链表比用数组合适,因为数组在头部插入需要O(n)搬移元素。
栈和队列的经典考法是用两个栈实现队列,或者反过来用两个队列实现栈。这个考点在选择题和编程题里都出现过。我的建议是把这两种互相实现的代码都亲手写一遍,不要只看思路,因为里面容易出错的是“倒腾数据的时机”。比如两个栈实现队列时,只有出队操作才把入栈的数据搬到出栈,而不是每入队一次就搬一次。
二叉树部分考的是遍历方式和递归理解。笔试喜欢让你根据前序+中序遍历还原二叉树,或者是判断某棵二叉树是完全二叉树还是满二叉树。前序+中序还原二叉树这类题没有难度,纯熟练度问题,建议刷十道找手感。完全二叉树的性质需要记住:除了最后一层,每层都是满的,最后一层节点从左到右连续排列。
3. 编程题实操与完整实现
3.1 第一道大题:反转链表,你写对了吗
2020年这套题的编程题部分,核心是考察链表、字符串、动态规划这几个方向。第一道大题非常经典:反转一个单链表。题目会给出链表节点定义,要求你实现函数反转链表并返回新的头节点。
这道题有两个主流解法。第一个是迭代法,思路是维护三个指针pre、cur、next,每一步把当前节点的next指向前一个节点。代码长这样:
struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* pre = nullptr; ListNode* cur = head; while (cur != nullptr) { ListNode* next = cur->next; // 先保存下一个节点 cur->next = pre; // 反转指针 pre = cur; // pre 前进 cur = next; // cur 前进 } return pre; }注意一个很常见的坑:在cur->next = pre之前,必须先把cur->next的原始值存下来。如果你直接修改了next,再想往后遍历就断了。这个错误几乎每个新手都犯过,而且笔试紧张时更容易出现。写完以后我建议你立刻在草稿纸上模拟一遍:1->2->3->nullptr,看到底每一步之后pre和cur分别在哪。
第二个解法是递归。递归的终止条件是当前节点为空或者下一个节点为空,直接返回当前节点。递归体的核心逻辑是:把当前节点的下一个节点的next指向当前节点,然后把当前节点的next置空。递归版本的代码更短,但对递归理解不深的同学容易写错终止条件,每次递归调用的返回值也没接住。我建议基础一般的人优先用迭代法,省时省心。
3.2 第二道大题:最长无重复字符子串
第二道编程题在当年出现频率也很高:给定一个字符串,找出其中不含重复字符的最长子串长度。比如输入"abcabcbb",答案是3,对应子串"abc"。这道题的标准解法是滑动窗口,配合哈希表做字符去重。
我直接给一份可运行的Java版本:
public int lengthOfLongestSubstring(String s) { HashMap<Character, Integer> map = new HashMap<>(); int left = 0; int maxLen = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (map.containsKey(c)) { // 如果重复字符在当前窗口内,把左边界移动到重复位置的下一位 left = Math.max(left, map.get(c) + 1); } map.put(c, right); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; }这个解法的时间复杂度是O(n),空间复杂度O(m),m是字符集大小。笔试时如果你只写出暴力双重循环的版本,能拿到一部分分数,但很难拿满分。原因很简单,O(n²)的暴力解法在字符串长度达到10^5级别时直接超时。
有一个细节值得单独说:为什么用left = Math.max(left, map.get(c) + 1),而不是直接left = map.get(c) + 1。因为map.get(c)返回的可能是上一次出现这个字符的位置,而这个位置可能在当前left左边。如果直接更新left,窗口左边界就可能倒退,导致计算出来的窗口长度变大。这个边界处理是本题最容易扣分的点。
3.3 编程题的提效技巧:先想清楚再动手
编程题部分最大的教训是:不要一上来就写代码。我见过太多同学拿到反转链表题目后立刻开写,写到一半发现next指针丢了,然后开始涂改。正确的步骤是先在注释里写清楚思路,再写代码。比如反转链表,你可以先在代码上方写三步:1.保存下一个节点;2.当前节点指向前驱;3.移动前驱和当前。这样编码时思路不会断。
另一个技巧是注意题目的边界输入。笔试时非常喜欢用空指针、空字符串、单节点链表这些极端情况来卡你的代码。写完之后第一件事不是提交,而是把边界case在脑子里过一遍。比如反转链表输入空指针,迭代法的while循环根本不会进去,直接返回pre也就是nullptr,这个是符合预期的。最长无重复子串输入空字符串,循环不会执行,maxLen保持0,也是对的。
如果你还想提前练习,建议把字符串转整数、合并两个有序链表、二叉树层序遍历这几道题也一起刷了。它们和上面两道题一样,都是校招笔试里反复出现的基础高频题,掌握了它们再去应对大多数公司的软开笔试编程题,基本不会慌。
4. 智力题与逻辑题实录
4.1 经典智力题:100层楼扔鸡蛋
问答题部分,2020年的卷子里出现过一些经典智力题,比如有两个鸡蛋,如何用最少的次数确定鸡蛋刚好摔碎的临界楼层。这道题堪称校招智力题的扛把子,解法有很强的代表性。
解这道题的关键是转变思路:不要一层一层试,也不要二分法。正确的优化方向是“每增加一次尝试次数,能覆盖的最大楼层数是多少”。假设我们允许扔m次,那第一次可以选择在第m层扔。如果鸡蛋碎了,剩下一个鸡蛋只能从第1层到第m-1层逐层试验,最坏情况需要m-1次,加起来是m次。如果鸡蛋没碎,我们还有m-1次机会,下一次可以往上再加m-1层,也就是从第m+(m-1)层继续。依次类推,能覆盖的总楼层数是m + (m-1) + (m-2) + ... + 1。这个表达式大于等于100即可。也就是m(m+1)/2 >= 100,解出来m最小是14。
这种题的通用解题心法是:最优策略不是等概率分段,而是让“未来剩余空间的代价”保持一致。说白了就是动态规划思想。笔试答题时,你不需要真的把一个复杂算法写完,但你要把思路表达清楚:用最坏情况下尝试次数最小化的模型,列出递推关系和边界条件。改卷人看的是有没有建模能力,而不是最终这个14是否算对。
4.2 这类智力题的通用解答框架
问到智力题,很多人容易慌。其实框架非常固定:先读清楚题目里的资源限制,比如鸡蛋数量、时间限制、测量次数;再考虑最坏情况是什么;最后用动态规划或者数学归纳法去逼近边界。这个框架能解大部分校招智力题,比如“25匹马找最快的3匹”“200个瓶子中有一瓶毒药”这类题。
拿“25匹马找最快的3匹”举例子,它的核心限制是每次只能跑5匹。第一步分成5组比赛,每组取前三;第二步让每组第一名比一次,确定第一名;第三步只需要考虑有可能进前三的几匹马再比一轮。这种题的考点就是“信息的最大利用”,它考察你对筛选逻辑的严密性。
遇到没见过的智力题,不要追求一步到位。先在草稿纸把暴力方案写出来,然后一步步优化,这本身就是展示思路的过程。问答题不完全看结果,过程分占比很大。
5. 常见的坑和排查技巧实录
5.1 笔试现场最容易踩的坑
我结合自己和周围同学的经验,把校招笔试现场最容易丢分的坑整理成了下面这张表:
| 坑 | 后果 | 规避方法 |
|---|---|---|
| 选择题耗时过多 | 编程题时间不足 | 遇到卡壳超过2分钟的题先标记跳过 |
| 编程题不处理边界输入 | 运行时直接异常或错误结果 | 写完先跑空值、单元素、超大输入 |
| 用错语言版本特性 | 本地能过在线编译不过 | 提前确认OJ支持的编译器标准 |
| 数组越界 | 运行时崩或返回错误答案 | 循环内反复检查索引边界 |
| 多写不必要的复杂解法 | 代码出现低级bug | 优先写你有把握的解法而不是最炫的解法 |
| 题目没读完就动手 | 回答方向整体跑偏 | 先花30秒圈出题目所有约束条件 |
这些都是血泪经验。比如选择题跳过策略,我见过不少同学在单选题上死磕一道题,结果最后编程题只能慌乱提交一个半成品。别把考场当研讨室,你的目标是总分最大化,不是每道题都满分。
5.2 编程题写完后的自检方法
写完代码后怎么自检,这是很多人忽略的环节。我给自己定的流程是:先去检查极端输入,再去检查常规输入,最后检查大输入量下的时间可行性。用反转链表举例,输入一个nullptr,在脑海里走一遍循环,确认不崩;输入一个节点,确认返回的是这个节点本身;输入两个节点,确认反转后顺序正确。这些自检可以在两分钟内完成,却能帮你规避掉大部分非预期错误。
最长无重复字符子串这道题,自检时建议用"pwwkew"这个例子。它的正确答案是3,对应子串是"wke"而不是"pwke",因为后者含有重复字符w。这个例子可以快速检验你的滑动窗口边界更新是否正确。哪怕是在线上IDE里临时跑一下,也不吃亏。
还有一个通用技巧:给自己写一个打印日志的临时版本,把滑动的left、right、maxLen每一步都打出来,和手推答案对比。这招调试字符串类的题目特别好用。提交前把日志代码删掉就行。
5.3 备战建议与刷题节奏
最后给正在准备软开校招的同学一个执行性建议。不要把战线拉太长,一个月足够。第一周主攻数据结构与算法,按链表、栈、队列、二叉树、排序、动态规划的顺序,每天三道题,保证数量。第二周开始加计算机网络和操作系统的基础复习,每天早晚各一小时,重点做选择题,用错题记录查漏。第三周刷整套模拟卷,掐表练习,练习时间分配。第四周复盘错题,把每道错题背后关联的知识点重新梳理一遍。
编程语言建议选你最熟的一门,Java或C++都行,但要求你能够不查文档写出链表节点定义、HashMap的常用方法、字符串处理这几个基本操作。笔试现场频繁查文档是很影响节奏的,还会显得基础不扎实。
我个人带同学复盘这套小米笔试题时,最大的体会是:它不考天才,考执行力。大部分题目只要系统复习过,都能做出来。真正的分水岭出现在时间管理、边界处理和答题策略上。你不需要在任何一个方向上是专家,但你需要确保每个基础方向都不掉链子。备考期间保持每天稳定刷题和总结的节奏,比考前突击一周管用得多。