秋招季最磨人的不是简历石沉大海,而是好不容易收到笔试通知,打开试卷却发现题目风格和预期完全对不上。今年小米秋招笔试软件开发卷2出来之后,不少同学在群里吐槽难度波动大、题型分布诡异。作为刚经历过这场笔试的人,趁着记忆还热乎,我把整张卷子的题目类型、考点分布、答题思路完整复盘一遍。
这份复盘不是简单贴答案,而是还原我在考场上的完整思考链路:哪些题该果断放弃、哪些题必须稳拿、算法题怎么从暴力解一点点优化到最优解、基础题里藏着哪些小米特有的“硬件基因”考点。无论你是准备投递小米,还是想摸底大厂笔试难度,这份复盘都能给你一个比较准确的参照系。
1. 拆卷之后的第一眼印象:题量、分值与答题顺序决策
1.1 整张卷子的结构
小米的秋招笔试用的是第三方在线测评平台,进入页面后自动全屏,切屏次数过多会触发警告。卷2的题型分布大概是这样的:
- 第一部分:单选题,约20道
- 第二部分:多选题,约5道
- 第三部分:编程题,2道
总分100分,选择题共占40分左右,编程题两道各占30分,合计60分。这个分值配比值得玩味——小米显然更看重代码实操能力,而不是死记硬背的基础知识。
时间限制是90分钟,但实际体感比这紧张得多。单选和多选一共25道,看起来数量不多,但有几道题涉及复杂计算和代码追踪,每道可能要耗5分钟以上。编程题两道的难度都不低,至少需要预留50分钟才能稳妥完成。所以我拿到试卷后的第一个决策是:选择题按“1分钟1题”的节奏快速过,遇到卡壳超过3分钟的先标记跳过去,绝对不会在选择题上恋战。
1.2 选择题的高频考点统计
根据回忆,选择题的考点主要集中在这几个方向:
| 考点模块 | 出现频次 | 典型题型 |
|---|---|---|
| 数据结构基础 | 5题 | 栈、队列、二叉树遍历、哈希冲突 |
| C++语言特性 | 4题 | 虚函数、智能指针、拷贝构造 |
| 操作系统 | 4题 | 进程线程、死锁、内存管理 |
| 计算机网络 | 3题 | TCP握手、IP地址、HTTP状态码 |
| 数据库 | 2题 | 索引原理、事务隔离级别 |
| Linux基础 | 2题 | 文件权限、常用命令 |
| 数学逻辑 | 3题 | 概率计算、贪心策略 |
这个分布其实很典型:无论哪家大厂,数据结构和语言基础永远是选择题的重头戏。操作系统和计算机网络作为计算机基础的核心,占比稳定在20%左右。数学逻辑题的出现说明小米对候选人的逻辑推理能力有要求。
1.3 时间分配上的真实操作
我在做选择题时采用了“三轮法”:第一轮把能秒答的题快速做掉,多数是数据结构和语言基础类;第二轮回头啃计算量大的操作系统、计算机网络题;第三轮才对个别拿不准的数学逻辑题进行“基于选项特征”的合理猜测。
这个策略的效果是:25道选择题耗时约35分钟,还剩55分钟分配给两道编程题——这个缓冲空间在后面的实战中起到了决定性作用。
2. 算法题的三道坎:暴力解法到最优解的现场取舍
2.1 第一道编程题:数组变换的最小操作次数
这道题题目描述大概是:给定一个整数数组,每次操作可以选择一个元素,将其除以2(向下取整)。问最少经过多少次操作,能使数组中所有元素的和不超过某个给定阈值。
乍一看,这题很像“贪心+最大堆”的经典套路。我当时的分析过程是:
每次操作对数组和的影响是:把某个数x变成x/2(向下取整),数组总和减少的量为x - x/2 = ceil(x/2)。要让总操作次数最少,等价于每次选择减少量最大的那个元素进行操作。
import heapq def min_operations(nums, target): total = sum(nums) if total <= target: return 0 # 最大堆,存入负数 heap = [-num for num in nums] heapq.heapify(heap) cnt = 0 while total > target: x = -heapq.heappop(heap) total -= x - x // 2 heapq.heappush(heap, -(x // 2)) cnt += 1 return cnt复杂度是O(n + k log n),其中k为操作次数。这道题本身难度中等偏易,关键在于能不能快速识别出“每次取最大”的贪心策略,并熟练用堆实现。实测下来,核心陷阱在于:当x为奇数时,x // 2 和 x // 2 的更新到底用整除还是浮点数,这里必须时刻保持整型运算,避免浮点数精度问题。
对于这一题的稳健解法,其实还有一个优化点:如果数组长度很大、目标阈值很低、元素数值很大,操作次数可能非常多,此时可以用桶排序的思想按数值区间批量处理,减少堆操作的log开销。不过笔试环境下,标准堆解法已经足够AC。
2.2 第二道编程题:二叉树的层序锯齿遍历
这道题要求按“之字形”顺序遍历二叉树:第一层从左到右,第二层从右到左,第三层再从左到右,依此类推。
这道题属于必须拿稳的经典题。看到题目的第一反应是BFS队列逐层遍历,用一个布尔变量记录当前层是否需要反转,需要反转时将当前层的节点值列表reverse后再加入结果集。这个解法的时间复杂度O(n),空间复杂度O(n)。
from collections import deque def zigzagLevelOrder(root): if not root: return [] res = [] q = deque([root]) left_to_right = True while q: level_size = len(q) level_vals = [] for _ in range(level_size): node = q.popleft() level_vals.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) if not left_to_right: level_vals.reverse() res.append(level_vals) left_to_right = not left_to_right return res这道题还有一个更“节省”的实现方式——在每层入队时按相反顺序入子节点,但边界条件很容易出错,实际笔试完全不建议冒险。直接遍历后反转(或使用双端队列从两端插入)是最稳妥的选择。
提示:笔试中算法题的判题标准通常包含边界用例,如空树、单节点树、节点值为负数等。提交前务必对极端场景做个快速自查。
2.3 算法题之外的隐含考察点
两道编程题做完,我回头看了一下代码,发现小米的在线编程环境有两点和其他大厂不一样:
第一,默认代码模板非常啰嗦,包含了大量注释和边界检查代码,提交时如果没删除无关输出,很容易导致编译错误或格式错误。第二,输入输出的样例格式容易误导人——题目给的是一个比较宽松的描述,但实际测试用例可能非常严格,比如树节点的输入是以层序遍历数组形式给出,空节点用null标记,解析时需要对连续null做特殊处理。
另外值得一提的是,小米的判题系统对时间限制的设置比较紧。第一道题如果用排序后逐个减的暴力方法,碰到极端数据大概率超时。笔试环境不能像本地IDE一样随意调试,合理利用注释和打印来调试是关键。
所以,平时练习刷题时最好养成先明确数据范围和复杂度约束,再动手编码的习惯,而不要拿到题就开始写循环。
3. 基础题里的“小米特色”:操作系统、网络与硬件结合点
3.1 操作系统题的考察角度比想象中更“底层”
选择题中操作系统部分出现了不少与硬件结合紧密的题。比如有一道题问:在操作系统启动过程中,CPU从复位到执行用户进程,经历了哪些关键模式切换?选项涉及实模式、保护模式、中断描述符表等。
这题如果只是背过“进程调度算法”和“虚拟内存”是答不出来的,必须对Intel x86架构下CPU的模式切换有基本了解。小米是做硬件起家的,招聘软件开发工程师时,对Android底层、驱动、BSP等相关方向有持续的需求,所以笔试中考到这类底层的系统启动流程并不意外。
另一道印象深刻的题目是:
关于Linux的进程调度,下列说法正确的是? A. CFS调度器是基于时间片轮转的 B. 实时进程的优先级高于普通进程 C. nice值越小,优先级越高 D. 时间片耗尽时进程进入僵尸状态这道题的关键是区分“正确”和“完全正确”。CFS(完全公平调度器)实际上是基于虚拟运行时间来进行调度,不是简单的固定时间片轮转,所以A是错的。B看起来对,但“实时进程优先级高于普通进程”不够严谨,应该说是“实时进程的调度策略优先级高于普通进程”,在某些特殊配置下并不是绝对成立。C是对的,nice值范围是-20到19,越小优先级越高。D很有迷惑性,时间片耗尽应该进入就绪状态等待调度,而不是僵尸状态。正确答案是C。
3.2 计算机网络题的细节陷阱
网络部分有三道题,其中一道是TCP三次握手的变体:
在TCP连接建立过程中,客户端发送的第二次握手报文(SYN+ACK)同时属于: A. 客户端发送给服务端的确认报文 B. 服务端发送给客户端的SYN请求 C. 服务端发送给客户端的ACK确认 D. 以上全部正确答案是D。二次握手报文是服务端在收到客户端的SYN之后,回复SYN+ACK,它既是对客户端SYN的确认(ACK),同时自身也携带SYN标志来请求同步。很多同学容易把三次握手的报文归属搞混,特别是在“这个报文是谁发给谁的”这种细节上。这里建议记住一个口诀:第三次握手是纯ACK,第一次握手是纯SYN,第二次握手是SYN+ACK。
另一道题考了HTTP状态码的语义:
当服务器理解请求但拒绝执行时,应返回的状态码是? A. 400 Bad Request B. 401 Unauthorized C. 403 Forbidden D. 404 Not Found答案是C。400是语法错误,401是未认证,403是已认证但无权限,404是资源不存在。这道题其实考验的是对HTTP协议语义边界的理解,如果只看过一遍状态码表而没有深入理解“401和403的区别”,很容易在这道题上翻车。
3.3 数据库索引的“反直觉”题
数据库题有两道,其中一道关于索引的题目比较有意思:
在InnoDB存储引擎中,关于聚集索引(Clustered Index)的说法正确的是? A. 每张表可以有多个聚集索引 B. 聚集索引的叶子节点存储的是整行数据 C. 二级索引的叶子节点存储的是指向数据的物理地址 D. 使用聚集索引查询一定比全表扫描快这道题的正确答案是B。InnoDB的聚集索引就是主键索引,一张表只能有一个聚集索引,叶子节点存放整行数据。C是易错项,二级索引的叶子节点存储的是主键值,而不是物理地址——如果要通过二级索引查找完整行数据,还需要“回表”到聚集索引中再查一次。D是典型的绝对化表述陷阱,“一定比全表扫描快”这句话在任何涉及数据库的面试题里出现都要高度警惕,当查询条件的选择性很差,或者表数据量很小时,优化器完全可能选择全表扫描。
3.4 Linux题和C++题的基础盘
Linux相关的两道题都是送分题级别:文件权限chmod 644对应的权限是“属主读写,属组读,其他读”;查看端口监听状态用netstat或ss命令。这类题目只要平时接触过服务器,基本都能秒答。
C++题里有一道虚函数相关的问题值得记录:
含有纯虚函数的类称为抽象类,以下说法正确的是? A. 抽象类可以实例化 B. 抽象类不能作为基类 C. 派生类必须实现所有纯虚函数才能被实例化 D. 抽象类可以定义对象数组正确答案是C。抽象类不能实例化,但可以作为基类;派生类只有实现了所有纯虚函数之后,才不再是抽象类,才能实例化。C++多态的基础是虚函数表(vtable),只要类中有虚函数,编译器就会为这个类生成一个虚函数表,对象的内存布局中会多一个指向vtable的虚指针,这是常见八股但也是理解多态的前提。
4. 编程题的实战拉锯:从调试崩溃到AC的完整过程
4.1 第一道题在堆的边界条件上栽了跟头
第一道数组变换题我写得很顺,但提交之后发现只通过了60%的用例。当时第一反应是算法有问题,但仔细分析后发现:每次减少量为x - x//2,如果数组中有0元素,堆顶弹出0之后,0//2还是0,程序会陷入死循环,total永远不会低于阈值。
这是我第一次栽在边界条件上。修正方法是:在弹出堆顶时判断如果x <= 1,说明当前数组已经不能再继续减小了,此时直接返回-1(表示无法达到目标)。
if x <= 1: return -1这个边界条件在题目描述中并没有明确说明,但测试用例显然包含了“不可能达成目标”的场景。这提醒我:读题后先花10秒思考极端情况——数组全0、只有一个元素、目标为负数、元素大于2^31,做这类检查比自己写完后再回头看要高效得多。
4.2 第二道题在输入解析上耗费了时间
二叉树的输入样例是以数组形式给出的,比如[1,2,3,null,null,4,5]。一开始我试图用递归方式还原二叉树,但Python的递归深度限制在这种场景下是个隐患。后来改成用队列迭代建树,虽然代码量多了一些,但逻辑更加直观,也不会有递归栈溢出风险。
这道题的另一个坑是“空节点到底怎么表示”——有时候题目写null,有时候写#,有时候写-1。虽然题目描述中有说明,但在紧张的考试环境下很容易看漏。稳妥做法是写一个通用的解析函数,不依赖具体的空值标记,而是在循环中判断字符串是否为常见空值标记。
4.3 本地测试和在线判题的差异
笔试结束后我复盘时发现,两道题里我各写了一版“能过本地样例”却“在线判题崩溃”的代码。主要原因:
一是本地Python版本和在线环境版本不一致。比如说我在本地用Python 3.10跑通的代码,在线环境可能用的是3.8,一些语法特性不兼容。笔试环境不让选择语言版本时,最稳妥的做法是只用最基本的语法,不要依赖新特性,如字典合并操作符|、内置泛型list[int]等。
二是死循环检测机制。在线判题系统通常有超时限制,如果代码在某个边界用例下陷入死循环,会直接判超时而不是给你一个清晰的错误信息。这时候只能靠人工审查代码逻辑来定位问题,无法像本地IDE一样查看当前执行到哪一行。
三是输出敏感性问题。在线判题系统对输出格式非常敏感,多打印一个空格、少打印一个换行都可能导致Wrong Answer。建议在提交前把代码里的调试打印全部删除或注释掉,并严格按照题目给的输出格式进行提交。
5. 错题背后的知识盲区与针对性补强
5.1 在智能指针题上暴露的现代C++短板
这个知识点在考后复盘时反复提醒我,因为问题本身很有迷惑性:
以下关于std::shared_ptr的说法,错误的是? A. 多个shared_ptr可以指向同一个对象 B. shared_ptr使用引用计数来管理对象的生命周期 C. 两个shared_ptr互相引用时会导致内存泄漏 D. shared_ptr可以安全地在多线程中对同一个对象进行拷贝和析构正确答案是D。shared_ptr的控制块包含引用计数,但这个计数器的操作虽然说是原子的,同一个shared_ptr对象被多个线程同时调用拷贝构造或析构,并不保证线程安全——线程安全指的是多个shared_ptr实例指向同一个对象时,它们各自管理自己的引用计数副本是安全的,但同一个实例的并发修改仍然需要外部同步。
这道题暴露了我对“引用计数线程安全性”的理解只停留在表层。现代C++工程中,智能指针是基础中的基础,但这个知识点在平时的刷题和八股中很少被深入问到。笔试结束后,我特意把《Effective Modern C++》中智能指针相关章节重新啃了一遍,把shared_ptr、unique_ptr、weak_ptr的使用场景、性能开销、线程安全性整理成了一张对照表。
5.2 概率推理题的“数学味”比预期浓
小米的数学逻辑题不像互联网大厂常规的行测逻辑,而是更偏向计算机科学中常用的概率分析。有一道题印象很深:
某系统有4个独立模块,每个模块正常运行的概率为0.9,只要任意3个模块正常,系统就能运行,问系统正常运行的概率?这个实际上是4选3的可靠性模型。正确解法是:计算恰好4个正常的概率加上恰好3个正常的概率。
P = C(4,4) * 0.9^4 + C(4,3) * 0.9^3 * 0.1 = 0.6561 + 0.2916 = 0.9477
答案是0.9477,约94.77%。这道题本身不难,但要求能快速反应出“至少3个正常”等价于“4个全正常+恰好3个正常”。如果平时没做过这类可靠性模型题,现场容易陷入不必要的复杂计算中。
5.3 一道变形版的哲学家就餐问题
多选题里有一道关于死锁的题,不是直接问“死锁的四个必要条件”,而是给了一个场景:5个哲学家围坐一桌,每人左手拿一个叉子,右手拿一个叉子,但叉子只有5把。问这个场景中发生死锁的根本原因是什么。
这道题考的就是死锁的“循环等待”条件。选项包括: A. 互斥条件 B. 持有并等待 C. 不可剥夺 D. 循环等待
答案是全选。五个哲学家都先拿左手叉子,再去拿右手叉子,会形成循环等待。但如果改成“先拿编号小的一边”,就能破坏循环等待,避免死锁。这个考点本身不难,难的是以场景题形式出现时,很多同学容易漏选“互斥条件”或“不可剥夺条件”。
做这类多选题的教训是:不要只盯着明显的选项,要把死锁的四个必要条件逐一和题干条件核对,逐个验证后再做选择。
6. 这份卷子给我带来的备考策略调整
6.1 基础题的重心从“广度”转向“深度”
小米卷2的基础题覆盖面不算特别广,但每道题都在往深处挖掘。比如操作系统考到CPU启动流程、数据库考到二级索引的回表机制、C++考到智能指针跨线程操作安全性。这些都不是那种“背一背就能答”的浅层内容,而是需要真的理解底层运作机制才能答对。
所以后续备考大厂笔试时,我调整了自己的复习重心——不再追求“把所有八股题背熟”,而是对每个高频考点下沉一层。比如面试的OS高频考点“虚拟内存”,不只背“页表、缺页中断、LRU置换”,而是追问自己:64位系统下的四级页表怎么寻址?TLB miss后硬件和操作系统各自做什么?这种下沉式复习虽然慢,但应付这类“深挖型”选择题反而更高效。
6.2 编程题的训练模式改为“限时手写+极端用例自查”
这次笔试让我意识到,LeetCode式的在线刷题在“输入解析”“边界处理”“超时保护”方面的训练是远远不够的。为此我给自己制定了新的训练方案:
- 每周至少3次限时手写代码练习,用记事本而非IDE,模拟笔试环境
- 每道题完成后,必须写出“极端用例清单”——空输入、单元素、大数值、负数、目标不可达
- 代码中禁止使用语言新特性,除非确认在线判题环境支持
这样训练一段时间后,我发现自己在笔试中犯的“低级错误”明显变少,比如忘记处理空指针、数组越界、整型溢出等。
提示:如果投递的岗位偏向嵌入式或系统开发,编程题的侧重点会明显不同。小米的嵌入式软件开发工程师笔试经常会出现位操作、寄存器配置、内存对齐相关的题目,这类岗位的笔试准备方式要和纯软件开发区分开。
6.3 做题节奏和心态管理的复盘
90分钟做完25道选择题+2道编程题,节奏感非常重要。我个人比较舒服的节奏是:
| 时间段 | 任务 | 心态目标 |
|---|---|---|
| 0-10分钟 | 快速浏览全卷 | 不慌张,确认题型 |
| 10-35分钟 | 第一轮秒选题 | 不恋战,果断跳过 |
| 35-40分钟 | 第二轮啃难题 | 每题最多5分钟 |
| 40-55分钟 | 第三轮猜测 | 不空题,选项特征辅助 |
| 55-90分钟 | 编程题 | 先写对,再优化 |
这套节奏的核心逻辑是:先确保编程题有充足时间,而不是把时间耗在选择题上。毕竟一道编程题30分,顶得上好几道选择题,投入产出比完全不同。
不过说实话,真正考试时我的节奏并没有执行得这么完美。第一道编程题因为边界条件多花了10分钟,导致第二道题的检查和优化时间被压缩。如果能重来一次,我会在写完第一道题后先提交一版“暴力解+能过部分用例”的代码,然后再提交优化版,这样即使后续优化失败,也不至于整个题丢分。
6.4 一些有价值的工具和资料整理
备考过程中我逐渐筛选出了一套比较高效的资料组合。
刷题方面,LeetCode的热题100和剑指Offer覆盖了大部分笔试算法题的类型,但建议按照数据结构类型进行专项练习,而不是随机刷题。比如集中三天只做二叉树相关的题目,再做动态规划,这种成体系训练对笔试的短期提分效果更明显。
基础知识的复习,可以参考小林coding的图解系统、图解网络,配合牛客上的历年大厂笔试题——但一定要警惕“背答案”的误区。笔试中同一道题往往会换一个场景、换一种问法,只有理解背后的原理,才不会被变化的外壳迷惑。
写在最后的一点个人体会
小米秋招软件开发卷2的整体风格是:基础扎实、算法不偏、底层味浓。没有那种故意刁难人的偏题怪题,但每道题都在考察“候选人是不是真的理解”而非“候选人是不是背过”。
如果你正在准备大厂秋招笔试,我个人的建议是:算法题练习固不可少,但一定不要忽视基础知识的深度理解。现在的大厂笔试越来越倾向于“反八股”——即题目看着眼熟,但提问角度刁钻,专治各种死记硬背。真正能拉开差距的,往往不是谁刷题多,而是谁能把知识点扎得更深、连得更广。
笔试只是秋招的第一道关,后面还有更残酷的面试。希望这份复盘能帮你少踩一些我踩过的坑,在真正的考场上更从容一些。