☰
小米秋招算法笔试复盘:KMP、堆排序与快速幂高频考点解析
2026/11/2 0:43:12 网站建设 项目流程

每次秋招季,算法岗的笔试都是重头戏,而小米的算法笔试在互联网大厂里算是风格比较有代表性的:题量适中、难度梯度明显、基础题和进阶题混着出,既考 coding 基本功,也考算法原理的理解深度。我身边不少朋友在 2021 年小米秋招算法方向第二场笔试结束后都在群里复盘吐槽,我把大家讨论最多的几类题目、解题思路和踩坑点整理了一遍,这篇文章就当作一份迟来的笔试复盘笔记。无论你是正在准备算法岗校招,还是想系统梳理一下笔试常考的算法知识点,这篇内容应该都能给你一些参考价值。

先说个总体结论:小米算法方向笔试并不是那种“硬刚难题”的风格,它更看重基础算法的熟练度、边界条件的敏感性,以及你在有限时间内能不能把思路转化成干净可跑的代码。只要把高频考点练熟,拿一个不错的分数并不难。

1. 笔试整体策略与考察范围拆解

1.1 这场笔试到底在考什么

先看考察范围。2021 年小米秋招算法方向第二场笔试覆盖的内容可以归成三大块:第一块是数据结构与基础算法,比如数组、链表、栈、队列、二叉树相关的操作,字符串匹配,排序与查找;第二块是进阶算法思想,包括动态规划、贪心、二分、双指针、回溯、图论基础;第三块则是机器学习/深度学习的基础概念,这部分更偏向选择题或者问答题,比如评价指标、损失函数、过拟合处理方式等。

从实际反馈来看,算法题主要集中在第一块和第二块,机器学习部分占比不算高,但一旦出现就比较考察概念是否扎实。比如“KMP 算法中 next 数组怎么求”“堆排序的时间复杂度是多少”这类题目,看起来是送分题,但很多人因为太久没看细节,反而在最基础的地方翻车。

我自己刷题复盘时的一个感受是:小米的笔试更偏爱“经典题目换个包装”的考法,比如把二分查找嵌到“旋转排序数组里找最小值”这种场景里,而不是直接让你写一个裸的二分。这其实比 LeetCode 上那些偏题怪题要友好得多,前提是你真的掌握套路,而不是背题。

1.2 题型构成与时间分配的复盘

根据当时参加笔试的同学回忆,整场考试大致由两部分组成:一部分是选择题/填空题,考察概念和简单计算;另一部分是编程题,需要在线编写代码并提交。选择题里会出现类似“在 KMP 算法中,对于模式串 p='abacaba',其 next 数组是?”这样的题目,编程题则以经典数据结构与算法题为主。

时间上,整场笔试大概两小时左右。我的建议是选择题控制在 30 到 40 分钟以内,后面编程题至少要留出 70 分钟。选择题如果真的卡住了,不要恋战,先凭直觉选一个并标记一下,回头再审。编程题部分时间分配要更精细,如果第一题想了 15 分钟还没思路,果断先跳过写后面的题。

还有一个容易被忽略的点:笔试环境通常是牛客网或者赛码网这类平台,代码提交后无法立刻看到全部测试用例结果,很多时候只能看到一个“通过率”。这种机制下,边界条件处理不好就很容易出现“只过一部分用例”的情况,非常可惜。

1.3 算法方向笔试的独特之处

算法方向的笔试和开发岗笔试最大的区别在于:它不止要求代码跑通,还要求你对复杂度有意识。同一个问题,用 O(n²) 的解法可能能通过小数据测试,但一旦测试用例变成 10 万级别就会超时。我在复盘时发现,很多同学不是不会做,而是解法太暴力,完全没有考虑数据规模。

比如有一道类似“求数组中第 K 大的数”的题,有人直接排序取下标,有人用堆,还有人用快速选择。三种解法在数据量小的时候都正确,但只有堆排序和快速选择在大数据量下稳定通过。这就是为什么复习时要同时关注“会做”和“做得高效”两个层面。

另外,机器学习算法相关的题也需要注意。比如“KNN 算法的应用能力包括哪几个方面”这种选择题,看似简单,但如果平时只是调包而不理解原理,很容易选漏。这类知识点建议在笔试前集中过一遍,不用太深,但基本概念要清晰。

2. 高频算法考点的原理与解题套路

2.1 字符串题:KMP 的 next 数组到底怎么推

字符串匹配是笔试高频考点,尤其是 KMP 算法。热搜词里有一句非常典型:“在 KMP 算法中,对于模式串 p='abacaba',其 next 数组(next[i] 定义为……”,这基本就是选择题的模板。

KMP 的核心思想是当匹配失败时,模式串不需要从头开始,而是利用已经匹配的前缀信息,把模式串向右滑动尽可能远的距离。这个“前缀信息”就存储在 next 数组中。next[i] 的定义一般是:模式串 p 的前 i 个字符组成的子串中,最长相等前缀和后缀的长度。注意,这个长度通常不包含子串本身。

以 p = "abacaba" 为例,我们手动推一下:

i = 0: next[0] = -1(有的实现里定义为 0 或 -1,不同教材定义不同,考场上看清题目要求) i = 1: 子串 "a",前缀后缀无相等,next[1] = 0 i = 2: 子串 "ab",前缀 "a",后缀 "b",不等,next[2] = 0 i = 3: 子串 "aba",前缀 "a" 和后缀 "a" 相等,长度为 1,next[3] = 1 i = 4: 子串 "abac",前缀 "ab" 和后缀 "ac" 不等,前缀 "a" 和后缀 "c" 不等,next[4] = 0 i = 5: 子串 "abaca",前缀 "a" 和后缀 "a" 相等,长度为 1,前缀 "ab" 和后缀 "ca" 不等,next[5] = 1 i = 6: 子串 "abacab",前缀 "ab" 和后缀 "ab" 相等,长度为 2,next[6] = 2 i = 7: 子串 "abacaba",前缀 "aba" 和后缀 "aba" 相等,长度为 3,next[7] = 3

所以 p = "abacaba" 的 next 数组是 [-1, 0, 0, 1, 0, 1, 2, 3]。如果你用的是“next[i] 表示失配时跳转位置”的版本,写法略有不同,但原理一致。

实操中我建议你掌握两种写法:一种是 next 数组整体右移一位,用 -1 开头,方便在匹配失败时直接跳转;另一种就是上面这种标准定义。考场上看到题目问 next 数组时,先确认题目给的是哪种定义,不然很容易答错。KMP 的时间复杂度是 O(n+m),这在笔试填空题里也是高频考点。

2.2 排序与堆:从冒泡到堆排的一次性打通

排序算法是笔试选择题的常客,也经常出现在编程题的底层实现里。搜索热词里“冒泡排序算法 C++”“堆排序算法”“快速排序”“排序算法”都在,说明大家对这块关注度很高。

冒泡排序适合做入门理解,但笔试里直接让你写冒泡的概率不大,更多是考它的时间复杂度、稳定性、最好最坏情况。冒泡最好情况 O(n)(加优化 flag),最坏和平均 O(n²),稳定排序。

堆排序是笔试编程题里的常客,尤其是“TopK 问题”和“第 K 大数”。堆排序的核心是两个操作:向下调整和建堆。建堆从最后一个非叶子节点开始,逐个向下调整,时间复杂度 O(n);每次取出堆顶后,把最后一个元素放到堆顶再向下调整,调整一次 O(log n),所以排序整体 O(n log n)。

我个人的建议是:堆排序不要死记硬背代码,而是记住“向下调整”这个核心函数。只要把这个函数写对了,建堆和排序都是围绕它转。代码模板我放到下一章详细写。

稳定性方面,堆排序、快速排序、选择排序都是不稳定的,归并排序、插入排序、冒泡排序是稳定的。这个知识点选择题必考,建议整理成表格记忆。

2.3 快速幂与位运算:有手就行的取模题

快速幂也是笔试里非常常见的考点,尤其是涉及大数取模的场景。热搜词里“快速幂算法 C++”出现得很高频,想必也是当年很多人搜过的内容。

快速幂的核心思想是把指数拆成二进制,利用幂运算的结合律减少乘法次数。比如计算 a^b mod m,朴素做法要乘 b 次,b 一大就超时;快速幂只需要 O(log b) 次。

原理很简单:把 b 写成二进制,比如 b = 13,二进制是 1101,那么 a^13 = a^8 * a^4 * a^1。我们只需要从 a^1 开始不断自乘,然后根据二进制位决定是否累乘到结果里。

模板代码如下:

long long quickPow(long long a, long long b, long long mod) { long long res = 1; while (b > 0) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res; }

这段代码里有几个细节要注意:一是每次乘法后都要取模,防止溢出;二是 res 初始化为 1,因为乘法的单位元是 1;三是 b >>= 1 相当于 b = b / 2,用位运算更快。掌握了这个模板,笔试里的快速幂题基本就是套模板。

2.4 二分、贪心、双指针与滑动窗口

这四个思想在笔试里出现频率极高,而且经常组合出题。

二分查找的难点不在模板,而在边界条件。我见过太多人因为 while (left < right) 还是 while (left <= right) 搞不清,导致死循环或者漏判。我自己常用的是左闭右闭写法:

int binarySearch(vector<int>& nums, int target) { int left = 0, right = nums.size() - 1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; else right = mid - 1; } return -1; }

left + (right - left) / 2 是为了防止 left + right 溢出,这在笔试中也可以作为一个加分细节。二分查找的变体很多,比如寻找左边界、右边界、旋转数组找最小值,建议把不同写法都练熟。

贪心算法的难点在于证明贪心策略的正确性。很多时候,直觉上觉得“每次取最大的/最小的”是对的,但笔试不会让你证明,只要通过测试用例就行。但要注意,贪心不是万能钥匙,遇到“看起来像贪心但总有一个用例过不了”的题,大概率需要换成动态规划。

双指针和滑动窗口就更常用了。滑动窗口的模板可以归纳为:右指针不断右移扩大窗口,当窗口不满足条件时,左指针右移缩小窗口,同时更新答案。这个模板在求解“无重复字符的最长子串”“最小覆盖子串”等问题时几乎可以套用。

3. 实操过程与关键代码实现

3.1 KMP 完整模板与边界验证

很多人在笔试前会背 KMP 模板,但一到考场上手写就容易在 next 数组的构建上出错。我建议你把构建 next 数组的代码单独拎出来,反复手写几遍,形成肌肉记忆。

下面是我自己常用的完整 KMP 模板:

#include <iostream> #include <vector> #include <string> using namespace std; // 构建 next 数组,next[i] 表示 p[0..i] 中最长相等前后缀长度 vector<int> buildNext(const string& p) { int m = p.size(); vector<int> next(m, 0); // next[0] 通常为 0,也可以根据定义设置为 -1 int j = 0; for (int i = 1; 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 主匹配逻辑 int kmpMatch(const string& s, const string& p) { int n = s.size(), m = p.size(); if (m == 0) return 0; vector<int> next = buildNext(p); int j = 0; for (int i = 0; i < n; i++) { while (j > 0 && s[i] != p[j]) { j = next[j - 1]; } if (s[i] == p[j]) { j++; } if (j == m) { return i - m + 1; // 返回第一个匹配的起始位置 } } return -1; }

这里有一个非常重要的边界细节:while (j > 0 && p[i] != p[j]) 中 j > 0 不能丢,否则 j 可能变成负数,导致数组越界。我当时就因为这个 bug 在笔试调试上浪费了不少时间。

如果你在考场上遇到的题目是要求返回所有匹配位置,而不是第一个位置,只需要在 j == m 时记录起始位置,然后把 j 重置为 next[j-1] 继续匹配,不要 break。

3.2 堆排序与 TopK 的现场写法

堆排序的核心是向下调整(heapify)。我建议用一个辅助函数维护大根堆或小根堆,然后根据需要选择排序还是取 TopK。

大根堆向下调整的模板:

void heapify(vector<int>& nums, int n, int i) { int largest = i; int left = 2 * i + 1; int right = 2 * i + 2; if (left < n && nums[left] > nums[largest]) largest = left; if (right < n && nums[right] > nums[largest]) largest = right; if (largest != i) { swap(nums[i], nums[largest]); heapify(nums, n, largest); } } void heapSort(vector<int>& nums) { int n = nums.size(); // 建堆,从最后一个非叶子节点开始 for (int i = n / 2 - 1; i >= 0; i--) { heapify(nums, n, i); } // 依次取出堆顶,放到数组末尾 for (int i = n - 1; i > 0; i--) { swap(nums[0], nums[i]); heapify(nums, i, 0); } }

如果是求第 K 大的数,可以不用完全排序,而是维护一个大小为 K 的小根堆,遍历数组,当堆满且当前元素大于堆顶时,弹出堆顶,把当前元素入堆。遍历结束后,堆顶就是第 K 大的数。这样做的时间复杂度是 O(n log K),当 K 远小于 n 时比排序快很多。

我个人在实际笔试中更倾向于用优先队列(priority_queue)来完成 TopK 操作,因为 C++ 标准库封装好了,耗时更少。前提是你要清楚优先队列默认是大根堆,如果要小根堆需要传 greater<int>。

3.3 动态规划典型题与记忆化搜索

动态规划在算法岗笔试里基本属于必考题。2021 年小米这场笔试里,有同学反映出现了一道类似于“最长上升子序列”的变种题,也有人说遇到了“子数组最大和”的经典题。这些题目只要能定义好状态,写对转移方程,代码本身并不复杂。

以最长上升子序列为例,最经典的 O(n²) 做法是:

int lengthOfLIS(vector<int>& nums) { int n = nums.size(); vector<int> dp(n, 1); int res = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = max(dp[i], dp[j] + 1); } } res = max(res, dp[i]); } return res; }

如果数据规模到 10 万级别,O(n²) 会超时,这时候可以用“贪心 + 二分”优化到 O(n log n)。维护一个 tails 数组,tails[len] 表示长度为 len 的上升子序列的最小末尾值。遍历 nums,找到 tails 中第一个大于等于当前元素的位置,替换掉,如果找不到则追加。这个技巧属于进阶内容,但也值得掌握,因为笔试容易通过数据规模来卡暴力解法。

另外,记忆化搜索也是动态规划的一种实现方式。遇到“递归 + 大量重复子问题”的题,直接用数组缓存结果,能避免重复计算。很多人在笔试时遇到树形 DP 会懵,其实本质就是“树的遍历 + 状态数组”。

3.4 机器学习/深度学习基础题的速答思路

算法方向的笔试里出现机器学习相关题目非常正常,尤其是选择题。比如“KNN 算法的应用能力包括哪几个方面”“过拟合的解决方法有哪些”“SVM 的核函数有哪些”等。

这些题目不用写代码,但概念要清晰。我建议你在笔试前把几个基础问题的答案整理成自己的话术:

  • KNN 可以用于分类和回归,核心是“投票”和“取均值”,受 K 值选择和距离度量影响较大,需要对特征做标准化。
  • 决策树/随机森林/GBDT/XGBoost 的区别与联系,重点记忆 XGBoost 的损失函数里加入了正则化项,这是它能防止过拟合的重要原因。
  • 过拟合的常用解决方法:增加训练数据、正则化(L1/L2)、Dropout、早停法、数据增强、降低模型复杂度。
  • 评价指标:分类问题看准确率、精确率、召回率、F1、AUC;回归问题看 MSE、MAE、R²。

笔试中偶尔还会出现一些比较冷门的概念,比如“KL 散度与 ELBO 的关系”“模拟退火算法原理”“粒子群算法原理”,这些偏向于考察知识广度。如果你时间充裕,可以把这些算法的核心思想各用两三句话概括一下,不需要深挖数学推导,但至少要能说出“模拟退火是借鉴金属退火过程,以一定概率接受更差解来跳出局部最优”这类程度的话。

4. 笔试常见问题与排查技巧实录

4.1 面试官视角的高频失误点

我复盘了身边不少人做小米笔试时出错的案例,发现高频失误点非常集中。

第一个是审题不清。题目要求返回下标还是返回值?数组是否有序?输入是否可能为空?这些细节一旦漏掉,代码就在边界测试用例上翻车。比如题目说“如果不存在返回 -1”,结果你返回了 0,全错。

第二个是复杂度过高。很多人第一反应是暴力解法,但对于 10 万级别的数据,O(n²) 的解法基本会超时。我建议你在动手写代码之前,先看一眼数据规模,然后快速估算时间复杂度能不能过。如果数据规模在 10^5 以上,O(n²) 几乎必挂,O(n log n) 或者 O(n) 才安全。

第三个是数组越界和空指针问题。C++ 的数组越界不会直接报错,只会产生未定义行为,甚至可能让代码“看似正确”地跑过小数据,在大数据上突然崩溃。这就是为什么手写代码时要特别小心下标,尤其是 while 循环里同时用到 i 和 j 的时候。

4.2 时间超限与内存超限的定位思路

笔试平台一般会明确提示“运行超时”或者“内存超限”。遇到超时,先看自己的算法复杂度,如果明显过高,先优化算法;如果复杂度没问题,再看是不是存在无意义的重复计算,比如循环里频繁调用函数、频繁创建容器。

内存超限相对少见,但遇到时优先检查是否申请了远超需要的二维数组,比如 dp 数组开成 n x n,但实际只用到两行,可以考虑用滚动数组优化。

另外,有一个非常实用的小技巧:如果你不确定自己的代码会不会超时,可以在本地生成一个最大规模的数据跑一下,用计时函数测量耗时。笔试环境虽然不能随便调试,但本地排查思路是可以提前养成的。

4.3 知识盲区如何临时救场

笔试时遇到完全没思路的题,千万不要直接放弃。算法题往往有“部分分”机制,暴力解法可能只能通过一部分测试用例,但也能拿到部分分数。

我当时的一个经验是:先写一个最简单、最不可能出错的暴力解法,保证通过样例和一部分小数据用例。如果时间还有剩余,再尝试用更优的算法去优化。这比死磕一道题导致其他题目没时间做要明智得多。

另外,如果选择题遇到完全不会的概念,可以试试排除法。比如 KMP、堆排序这类概念题,四个选项里通常有两个明显错误,剩下两个再结合直觉判断,正确率会高很多。

4.4 刷题与复习的资料清单

最后聊一下复习资料。我当时准备秋招笔试时,主要用的是 LeetCode 的“热题 HOT 100”和“剑指 Offer”系列。高频知识点大概就是字符串、数组、链表、二叉树、动态规划、二分、贪心、堆、栈和队列。建议把每个知识点都找一个代表性题目,练到能默写核心代码的程度。

机器学习部分,如果时间紧就不用翻大部头教材,直接看一些总结性的面经或者博客,重点记忆基础概念和常用模型的优缺点。如果时间充裕,可以看《机器学习》(周志华)或《统计学习方法》(李航)里的前几章,把感知机、KNN、决策树、SVM 这些基础模型理一遍。

笔试前的最后一天,别再做新题了,把以前的错题和模板代码过一遍,尤其是 KMP、快速幂、堆排序、二分查找、动态规划这些核心代码块。我个人的经验是,睡前把每个模板在脑子里默背一遍,第二天上考场会安心很多。

写在最后

小米这场算法笔试给我的整体印象是“稳中有变”:基础题占了大部分,难度适中有区分度,但对基本功的要求非常明确。如果你把 KMP、堆排序、快速幂、二分、动态规划这五类高频考点练透,通过笔试的把握会大很多。再分享一个小建议:平时刷题一定要养成手写代码的习惯,不要总是依赖 IDE 的自动补全和编译提示。笔试环境里没有这些辅助,全靠基本功,手写代码的熟练度直接影响你的答题速度。

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

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

立即咨询