2021小米秋招算法笔试复盘:核心考点与编程题思路解析
2026/8/29 2:07:59 网站建设 项目流程

2021年小米秋招算法方向第一场笔试复盘

每年到了秋招季,算法岗笔试都是淘汰率最高的一关。2021年小米秋招算法方向第一场笔试,我是在牛客网线上完成的,整体感受是:单选题覆盖面广、编程题难度居中偏上,但区分度很清晰。这篇复盘我把当时的题型结构、核心考点、编程题思路和代码完整整理出来,给准备大厂算法岗笔试的同学一个可参考的复习坐标。不管你是2022届、2023届还是更后面的学弟学妹,只要目标是算法岗,这套题反映出来的考察倾向基本不会过时。

先说结论:小米算法笔试主要考三块——数据结构与算法基础(KMP、排序、树、图)、常见算法设计范式(贪心、分治、动态规划、搜索)、C++/Java语言细节和简单概率题。编程题一般是两到三道,我遇到的是两道:一道字符串处理,一道任务调度类问题。下面按题型逐块拆解。

1. 笔试整体结构与命题风格复盘

1.1 题型分布与分值逻辑

2021年小米秋招算法方向第一场笔试,整体分为两个部分:第一部分是单选题,第二部分是编程题。单选题大约20道左右,每道分值不高,但正确率直接决定能不能进下一轮面试。编程题一般是两道,少数场次有三道,每题有多个测试用例,输出格式严格,和LeetCode的核心代码模式不一样,小米笔试用的是ACM模式,也就是要自己处理标准输入输出,这一点很多人直接栽了。

单选题的覆盖范围很广,我印象比较深的有这么几类:KMP算法中next数组的计算、排序算法的时间复杂度与稳定性判断、二叉树遍历的递归与迭代写法、哈希表冲突处理、贪心算法的适用场景、概率统计基础题(比如抛硬币、随机变量期望)、C++的虚函数和内存布局。这意味着你在复习的时候不能只刷代码题,基础概念题必须过一遍,尤其是指针、引用、const、静态变量这些C++高频考点。

分值逻辑上,编程题占大头。两道题如果全过,基本就能稳进面试;如果只过一道,选择题正确率又在中等水平,就可能被卡在笔试线附近。所以我的建议是:选择题尽量保证正确率,编程题至少完整解出一道,并且通过所有样例。你不需要在笔试中做出所有的题,但一定要保证做出来的题是满分状态。

1.2 命题风格:为什么小米喜欢这么考

小米的算法笔试有一个明显特点:不搞偏题怪题,考察的都是经典模型,但会在边界条件和输入输出上做文章。比如字符串题你不会觉得没见过,但稍不注意就会在某些特殊输入上挂掉。这和大厂的筛选逻辑一致——面试官想知道的是你的基本功是否扎实、代码是否稳健,而不是你背了多少冷门算法。

从岗位方向上来说,“算法方向”在小米内部其实包含推荐、搜索、NLP、CV等多个子方向,但笔试是统一一套题,所以考的都是通用算法能力。你不会被问到“音频重采样算法”或“PID算法怎么调参”这种具体领域的问题,那些是后续业务面试才会涉及的内容。笔试阶段就是筛选:数据结构扎实不扎实,思路清不清楚,代码能不能一次写对。

复盘以后我强烈建议准备小米笔试的同学,不要只刷难题,而是把经典题刷透。比如无重复字符的最长子串、任务调度器、编辑距离、最长公共子序列、岛屿数量、二叉树层序遍历,这些题反复出现在小米、美团、百度、字节的笔试题里,它们是真正的“高频考点”。

2. 单选题高频考点拆解:KMP、排序与复杂度

2.1 KMP的next数组到底怎么推

KMP是笔试选择题里的常客,2021年小米这场就考了next数组的计算。题目大概是:对于模式串p = "abacaba",按照next[i]定义为子串p[0..i]的最长相等真前后缀长度,求对应的next数组。

我在这里先说说两种常见的next定义,因为很多教材和网上博客的定义都不太一样,你要是搞混了,选择题肯定错。

第一种定义(最长相等真前后缀长度):next[i]表示p[0..i]这个前缀子串中,最长的相等真前缀和真后缀的长度。比如p="abacaba":

  • i=0,子串"a",真前后缀为空,next[0]=0
  • i=1,子串"ab",前缀有"a",后缀有"b",不相等,next[1]=0
  • i=2,子串"aba",前缀有"a"、"ab",后缀有"ba"、"a",最长相等的是"a",长度1,next[2]=1
  • i=3,子串"abac",前缀"a"、后缀"c"不相等,next[3]=0
  • i=4,子串"abaca",前缀有"a"、"ab"、"aba"、"abac",后缀有"a"、"ca"、"aca"、"baca",最长相等的是"a",长度1,next[4]=1
  • i=5,子串"abacab",前缀有"a"、"ab"、"aba"、"abac"、"abaca",后缀有"b"、"ab"、"cab"、"acab"、"bacab",最长相等的是"ab",长度2,next[5]=2
  • i=6,子串"abacaba",前缀有"a"、"ab"、"aba"、"abac"、"abaca"、"abacab",后缀有"a"、"ba"、"aba"、"caba"、"acaba"、"bacaba",最长相等的是"aba",长度3,next[6]=3

所以next数组是[0, 0, 1, 0, 1, 2, 3]。

第二种定义(失配时跳转的位置):有的教材把next[i]定义为当p[i]匹配失败时,模式串指针应该跳到的位置。这种定义下next值会比前一种整体右移、有的位置还要减1,而且next[0]=-1。如果题目里没有明确说明“next[i]定义为最长相等真前后缀长度”,那你最好先看一下题目给的定义再计算。我在笔试时是先判断定义再动手,避免被这种“定义差异”坑到。

如果你想在考场上快速验证,可以手写一个求next数组的代码,心里模拟一遍:

vector<int> getNext(string p) { int m = p.size(); vector<int> next(m, 0); for (int i = 1, j = 0; i < m; i++) { while (j > 0 && p[i] != p[j]) j = next[j - 1]; if (p[i] == p[j]) j++; next[i] = j; } return next; }

这个代码对应的是第一种定义,也是力扣、牛客上最常见的写法。平时刷KMP相关题,把这一种写法吃透就够用了。

2.2 排序算法横向对比与笔试陷阱

选择题里排序算法也是必考项。小米这场我记得考了“堆排序建堆的时间复杂度”和“快速排序最坏时间复杂度”,这种题你要是只记结论不理解的,容易卡壳。我直接给出笔试最容易考的对比表:

排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性
冒泡排序O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(1)不稳定
插入排序O(n^2)O(n^2)O(1)稳定
希尔排序O(n^1.3) 左右O(n^2)O(1)不稳定
归并排序O(n log n)O(n log n)O(n)稳定
快速排序O(n log n)O(n^2)O(log n)(递归栈)不稳定
堆排序O(n log n)O(n log n)O(1)不稳定

最容易踩的坑有三个:第一,快排最坏情况是数组已经有序或逆序时,如果每次选的基准都是最大或最小元素,递归深度会退化到n,时间复杂度O(n^2)。笔试选择题经常把“快排平均O(n log n)、最坏O(n^2)”和“归并排序始终是O(n log n)”放在一起混淆,别选错。第二,堆排序建堆的时间复杂度是O(n),不是O(n log n)。你从最后一个非叶子节点开始向下调整,调整次数总和是O(n),这个结论很多人记混。第三,稳定性的判断方法是看“相等的元素在排序后是否保持原有相对顺序”,冒泡、插入、归并是稳定排序,选择、快排、堆排不稳定。

2.3 复杂度计算与主定理速查

小米笔试里有一类送分题是“给你一个递归式,问时间复杂度”,比如T(n) = 2T(n/2) + O(n)是多少。这种题用主定理可以秒杀。主定理针对形如T(n) = aT(n/b) + f(n)的递归式,比较f(n)和n^(log_b a)的增长率:

条件结论
f(n) < n^(log_b a)(多项式意义下小)T(n) = Θ(n^(log_b a))
f(n) ≈ n^(log_b a)T(n) = Θ(n^(log_b a) log n)
f(n) > n^(log_b a)(多项式意义下大),且满足正则条件T(n) = Θ(f(n))

比如归并排序是T(n)=2T(n/2)+O(n),n^(log_2 2)=n,属于第二种情况,所以T(n)=O(n log n)。二分查找T(n)=T(n/2)+O(1),n^(log_2 1)=1,也是第二种情况,T(n)=O(log n)。

需要提醒的是,很多选择题不会直接问主定理,而是把递归展开让你猜。这时候你脑子里要有一个预估:如果每次规模减半但只处理一次,一般就是O(log n);如果每次减半但处理两个子问题,就是O(n log n);如果规模只减1,一般是O(n^2)或O(2^n)。这些估算能力比死记结论更可靠,因为笔试现场的题往往稍微变形了一下。

3. 编程题真题思路还原与代码实现

编程题是笔试的重头戏。小米2021秋招算法方向第一场笔试的两道编程题,我根据自己的回忆和同届同学的交流,整理成下面两个考点一致的题目。题目描述我做了重新表述,但核心考点、数据范围和边界条件与原题保持同一水平。

3.1 第一题:无重复字符的最长子串

题目描述:给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。

示例:

  • 输入 s = "abcabcbb",输出3,因为最长子串是"abc"。
  • 输入 s = "bbbbb",输出1。
  • 输入 s = "pwwkew",输出3,最长子串是"wke"。

这道题是滑动窗口的经典题目。笔试遇到它,不要想复杂,直接用双指针维护一个窗口,右指针一直往右走,同时用一个哈希表记录窗口内每个字符最后出现的位置。每次遇到重复字符,就把左指针跳到重复字符上一次出现位置的下一个位置,过程中不断更新最大长度。

#include <bits/stdc++.h> using namespace std; int lengthOfLongestSubstring(string s) { vector<int> lastPos(128, -1); int left = 0, ans = 0; for (int right = 0; right < (int)s.size(); right++) { char c = s[right]; if (lastPos[c] >= left) { left = lastPos[c] + 1; } lastPos[c] = right; ans = max(ans, right - left + 1); } return ans; } int main() { string s; while (getline(cin, s)) { cout << lengthOfLongestSubstring(s) << endl; } return 0; }

两个细节值得注意:一是lastPos[c] >= left这个判断很关键,因为窗口在移动,哈希表里可能有窗口外的旧位置,不能用“是否等于-1”来判断是否重复,而要用“是否在窗口内”;二是输入可能有多行,所以用getline循环读取,这也是ACM模式常见的处理方式。

边界情况:空字符串返回0,单个字符返回1,全相同字符返回1。这道题时间复杂度O(n),空间复杂度O(128),实际可以认为O(1)。

3.2 第二题:任务调度器

题目描述:给定一个用字符数组tasks表示的任务列表,每个字符代表一种任务类型,相同任务之间必须间隔n个时间单位才能再次执行。每个单位时间可以执行一个任务或处于待命状态。请计算完成所有任务所需的最短时间。

示例:

  • tasks = ["A","A","A","B","B","B"],n = 2,输出8。
  • 执行顺序可以是A -> B -> 待命 -> A -> B -> 待命 -> A -> B。

这道题有两个子思路。第一个是贪心+数学推导:先统计每个任务的次数,找到出现次数最多的任务,设maxCnt为最大次数,maxNum为出现次数等于maxCnt的任务种类数。最短时间至少是(maxCnt - 1) * (n + 1) + maxNum,但结果不能小于任务总数。第二个思路是用最大堆模拟,每个时间单位取出当前可执行的任务中剩余次数最多的任务执行,再冷却n个时间单位后放回堆。

我建议笔试时优先用数学推导法,代码短、不容易错,复杂度O(n),而且能处理大数据量。堆模拟法更通用,但要小心处理“冷却中的任务还没到时间就放回堆”这种细节,写起来容易出bug。

#include <bits/stdc++.h> using namespace std; int leastInterval(vector<char>& tasks, int n) { vector<int> cnt(26, 0); for (char c : tasks) cnt[c - 'A']++; int maxCnt = *max_element(cnt.begin(), cnt.end()); int maxNum = 0; for (int i = 0; i < 26; i++) { if (cnt[i] == maxCnt) maxNum++; } int ans = (maxCnt - 1) * (n + 1) + maxNum; return max(ans, (int)tasks.size()); } int main() { string s; int n; while (cin >> s >> n) { vector<char> tasks(s.begin(), s.end()); cout << leastInterval(tasks, n) << endl; } return 0; }

这道题笔试时容易在输入格式上出错。tasks和n在一行输入,tasks是一个没有空格的字符串,所以用cin读取字符串再转换成vector 。如果你按“读一个整数n”的格式写,可能会读不到或死循环。另外,测试用例里可能包含空格分隔的任务列表,比如A B A,这时候用getline读取整行再按空格拆分更稳妥。

3.3 第三题:编辑距离

有些场次的笔试会加一道动态规划题,我当时这场没有遇到,但这里值得顺带整理,因为编辑距离在算法岗笔试里的出场率实在太高了。题目描述:给你两个单词word1和word2,请返回将word1转换成word2所使用的最少操作数。可以对一个单词进行三种操作:插入一个字符、删除一个字符、替换一个字符。

状态定义是dp[i][j]表示word1前i个字符转换成word2前j个字符的最小操作数。转移方程:如果word1[i-1] == word2[j-1],dp[i][j] = dp[i-1][j-1];否则dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1),分别对应删除、插入、替换。

#include <bits/stdc++.h> using namespace std; int minDistance(string word1, string word2) { int m = word1.size(), n = word2.size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1)); for (int i = 0; i <= m; i++) dp[i][0] = i; for (int j = 0; j <= n; j++) dp[0][j] = j; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1[i-1] == word2[j-1]) { dp[i][j] = dp[i-1][j-1]; } else { dp[i][j] = min({dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1}); } } } return dp[m][n]; } int main() { string word1, word2; while (cin >> word1 >> word2) { cout << minDistance(word1, word2) << endl; } return 0; }

时间复杂度和空间复杂度都是O(mn)。如果笔试时内存卡得紧,可以优化成一维数组,但考虑到时间是主要矛盾,我建议先把二维版本写对,再去想优化。考场上写出AC代码比写出“更优但可能写错”的代码重要得多。

4. 现场做题的节奏控制与细节经验

4.1 时间分配策略

小米笔试的总时长我记得是90分钟左右。选择题量不小,编程题又有难度,时间分配很重要。我当时的策略是:选择题控制在40分钟内,最多45分钟;编程题留45到50分钟。如果某道选择题卡了两三分钟还没头绪,先跳过,等编程题写完了再回来蒙一个。选择题的“性价比”远低于编程题,一道选择题可能就1到2分,而一道编程题可能占一半分数。

编程题的时间分配也要按“先易后难”来:先扫一眼两道题,选一道思路更清晰的先写,确保通过所有样例后再去攻坚第二道。我见过不少同学在第一道题上死磕优化,结果第二道送分题都没时间写,这是非常亏的。

4.2 容易被扣分的代码细节

笔试扣分往往不是因为思路不对,而是代码细节出问题。我整理了几个高频扣分点:

第一,输入输出没处理好。ACM模式下必须自己读数据、自己打印结果。很多同学平时用LeetCode的Solution类习惯了,笔试时忘了写main函数和输入循环,直接交上去编译器报错。你在刷题时就要有意识练习标准输入输出,尤其是处理多组测试用例、可能包含空行的情况。

第二,数组越界。滑动窗口类题目最容易在left和right的边界上出问题。建议在写循环时画一下窗口区间是左闭右闭还是左闭右开,再把边界条件写清楚。

第三,数据类型溢出。如果题目数据范围达到10^9以上,int可能不够用,要用long long。任务调度器这类题虽然用int够,但涉及乘法(maxCnt - 1) * (n + 1)时,最好提前想一想是否可能溢出。

第四,变量名混淆。我见过有同学把left和right写反,或者把dp[i-1][j-1]写成dp[i-1][j+1],这种问题在紧张时特别容易发生。写完代码后花30秒重新读一遍关键循环,检查下标。

4.3 ACM模式与核心代码模式的区别

力扣默认是核心代码模式,你只需要实现一个函数。但小米笔试是ACM模式,你需要自己写#include、main函数、读取输入和输出。我建议准备阶段就切换到ACM模式刷题,或者至少每周做几道牛客网的ACM模式题目。牛客网的“剑指offer”和“公司真题”板块基本都是ACM模式,用来练手很合适。

现场还有一个技巧:在你提交前,先在本地或在线编辑器里跑一遍示例输入,确认输出和题目给的一致。有时候题目会同时给多个示例,你把每个示例都跑一遍,不要只看第一个。我就见过示例1过了但示例2不过的情况,往往是边界条件没处理好。多跑几个用例再提交,省得反复提交扣罚时。

5. 高频算法点自查清单与复习方向

笔试结束后的复盘比笔试本身更有价值。我把自己在小米这场笔试中遇到的知识点,加上历年大厂算法笔试的高频考点整理成一张清单,你可以对照自查:

考点出现频率典型题型复习建议
滑动窗口极高无重复字符的最长子串、最小覆盖子串理解双指针移动逻辑,记住窗口内状态维护方式
哈希表极高两数之和、字母异位词分组掌握冲突处理、常用API
贪心算法极高任务调度器、跳跃游戏、分发饼干培养“局部最优推全局最优”的证明意识
动态规划极高编辑距离、最长公共子序列、打家劫舍30分钟能写出经典DP的转移方程
KMP算法next数组计算、字符串匹配能手推next数组,两种定义不要混
前K个高频元素、合并K个有序链表掌握priority_queue的使用和自定义比较器
二叉树层序遍历、最近公共祖先熟记递归和迭代两套写法
排序算法复杂度比较、稳定性判断横向对比表,能手写快排和归并
二分查找旋转数组找最小值、搜索插入位置注意边界开闭,复习lower_bound
图论岛屿数量、课程表(拓扑排序)掌握DFS/BFS,拓扑排序判环

我自己在复习时有一个习惯:每做完一类题,就在清单里打一个勾,并写下这道题用了什么套路。比如看到“最小”+“子数组”大概率是滑动窗口或前缀和;看到“最长”+“两个字符串”大概率是DP;看到“最短时间”可能涉及贪心或优先队列。这种“题目特征 -> 算法方向”的条件反射,在笔试有限时间内的价值非常大。

另外,如果你投的是偏推荐、搜索的算法岗,笔试后的面试中还可能被问到机器学习基础,比如KL散度、ELBO推导、聚类算法、BM25相关原理。但那是面试环节的事,笔试阶段先把数据结构与算法的基础题过关,战线拉得太长反而两头不讨好。

写在最后:笔试只是第一步,复盘才是涨分的关键

我做完小米这场笔试后的最大感受是:题目本身并不偏,但想要在90分钟内稳定写完、写对,靠的是平时积累的“肌肉记忆”。尤其是KMP的next数组、排序算法的稳定性、滑动窗口的边界处理这些细节,如果你在考场上还要想半天,基本就输了。

一个很实用的复盘方法:每场笔试结束后,不管结果如何,都把自己遇到的题目按“做对了”“做错了”“题意理解了但思路没想出来”分三类整理。做错的题分析是知识点缺失还是粗心;思路没想出来的题,去LeetCode或牛客找同类型题目补练三到五道。我整理这份清单也是希望帮你跳过踩坑的过程,直接对准高频考点发力。

最后再分享一个我在实际笔试中验证过的小技巧:编程题千万不要卡在输出格式上。如果你不确定是输出一个整数还是字符串、是否需要换行,就按题目示例的格式来,示例输出是什么形式就照着写。ACM模式判题通常忽略行末空格,但不会忽略多输出或少输出,所以最稳妥的做法是先打印题目给的示例,看输出是否完全一致,再提交。祝你能顺利通过笔试,咱们面试见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询