1. 先搞清楚笔试到底考什么:vivo算法岗题型与考察逻辑
1.1 2024秋招vivo算法类笔试的整体结构
我投的是vivo的CV算法岗,秋招笔试通知来得挺快,从投递到收到笔试链接大概隔了不到一周。整套卷子90分钟,题目量不大,但覆盖范围相当广,大致分成三块:选择题、问答题、两道编程题。
选择题大概10道左右,考的是数据结构和机器学习基础,比如给一棵二叉树让你推后序遍历、给一段快排代码问你时间复杂度、KNN的基本原理、过拟合的解决手段,偶尔还会冒出一道图像滤波的题。这部分难度不高,但很看基础牢不牢。问答题一般有两到三题,要求用文字描述某个算法的原理。我当时碰到的题目是“简述粒子群算法的基本流程”和“KMP算法中next数组的作用”。这种题没有标准答案,但阅卷的人能一眼看出你是真懂还是背概念。两到三道编程题是整场笔试的大头,难度适中:一道偏数据结构,一道偏思维。实测下来,把基础算法复习扎实的人,通过率会明显高一些。
1.2 算法岗笔试和互联网大厂的区别
很多同学习惯拿互联网大厂的题库去准备vivo,这种做法有个问题:大厂笔试特别喜欢考高难度动态规划和复杂状态压缩,而vivo的算法岗更偏向工程实践和行业场景。手机厂商的算法团队平时做什么?相机影像算法、音频降噪、AI端侧部署、搜索推荐、电源管理里的控制算法,这些都是实打实要和硬件打交道的方向。因此笔试选用的算法题目也更倾向于“基础但实用”。
比如热词里出现的BM25、PID、SOBEL、拉普拉斯锐化这些看起来杂七杂八的算法,背后正好对应了搜索排序、电源控制、图像处理这些手机厂商业务场景。我在复习时一开始只觉得这些东西零散,后来才意识到,这些就是vivo算法岗笔试真正想考察的核心能力:基础算法原理扎实、能理解工程算法场景、会动手写代码。
1.3 时间分配策略:90分钟怎么用
根据我的实操经验,笔试的时间分配建议这样:选择题最多25分钟,问答题25分钟,剩余40分钟留给编程题。编程题宁可做对一道、留下一道空着,也不要两道都写了一半。vivo的在线笔试系统通常支持本地IDE编译,但也不排除部分场次只提供网页编辑器,所以平时就要练手写代码的熟练度。
选择里如果卡壳超过2分钟,先随便选一个并标记,回头再看。问答题要写核心流程和关键公式,尤其是类似“粒子群算法的速度更新公式”这种硬核内容,写出来就是加分项。编程题先读清楚输入范围和边界条件,再动手。我见过太多人一上来就写,写到一半发现漏了空数组的情况,当场崩溃。
2. 高频考点精讲:从字符串到机器学习,这些算法必须滚瓜烂熟
2.1 字符串算法:KMP的next数组,手撕推导必须熟练
KMP算法在vivo笔试中出现的频率不低。模式串的next数组计算是必考基本功,值得反复练习,直到能闭着眼写出代码。
以热词里的模式串 p="abacaba"为例,我们计算它的next数组(这里采用0索引、next[i]表示前i个字符构成的子串中最长相等前后缀的长度):
- next[0] = 0,单个字符没有前后缀,长度为0。
- i=1,子串"ab",前缀"a",后缀"b",不相等,next[1]=0。
- i=2,子串"aba",前缀"a"等于后缀"a",长度1;同时"ab"不等于"ba",所以next[2]=1。
- i=3,子串"abac",前缀"a"对应后缀"c"不相等,前缀"ab"对应后缀"ac"不相等,继续往前找,next[3]=0。
- i=4,子串"abaca",前缀"a"等于后缀"a",长度1,next[4]=1。
- i=5,子串"abacab",前缀"ab"等于后缀"ab",长度2,next[5]=2。
- i=6,子串"abacaba",前缀"aba"等于后缀"aba",长度3,next[6]=3。
最终next数组就是 {0, 0, 1, 0, 1, 2, 3}。有些教材把next定义为失配跳转位置,那会在部分场景上偏移一位,考试时要看清题目定义,避免踩坑。
vector<int> buildNext(const string& p) { int m = p.size(); vector<int> next(m, 0); 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; }这段代码的精髓在于while循环里的回退。匹配失败时,不必从头开始,而是利用已经计算好的next数组跳回到上一个可能的匹配位置。KMP的时间复杂度是O(n+m),空间复杂度O(m),比暴力匹配稳定得多。
2.2 排序算法:快排归并堆排的复杂度表和适用场景
排序是笔试选择题的常客,经常会拿“以下哪种排序算法是稳定的”、“堆排序的最好最坏复杂度”来考。我整理了一张表,建议考前反复看:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
快排的平均性能最好,但最坏退化到O(n²)。如果笔试问“数据基本有序时用哪个排序最好”,插入排序是标准答案。归并排序虽然需要额外空间,但胜在稳定,适合外部排序。堆排序在需要取TopK的场景里很实用。刷题时我习惯背熟这些结论,再配合一两道手写排序题练手,笔试基本不会失分。
2.3 图论与动态规划:Dijkstra、贪心、DP怎么区分
Dijkstra算法是处理单源最短路径的经典算法。它的基本思想是维护一个dist数组,每次从未访问的节点中选一个距离最小的节点,然后更新它相邻节点的距离。朴素写法时间复杂度O(V²),用优先队列优化后可以降到O(E log V)。
不过要注意,Dijkstra不能处理负权边。笔试里如果遇到带负权的图,则要考虑Bellman-Ford或SPFA。我记得有一道模拟题真的很坑:图的权值都是正数,但问的是最长路径,很多人条件反射就套Dijkstra,结果全错。最长路径在有环图中是NP-Hard,不能用Dijkstra直接改,这点一定要记住。
动态规划和贪心的区别也是高频问答题。贪心是每步做局部最优选择,且不回头;DP则是枚举所有状态并记录最优子结构。经典例子:找零钱问题,如果硬币面额是1、5、11,要找15元,贪心会先选11再选4个1,一共5枚,但最优解是3个5,共3枚。这就是贪心失效的场景。笔试中遇到这种问题,一定要先判断贪心是否能证明正确性,否则就老老实实写DP。
2.4 群智能与搜索优化:粒子群、模拟退火、剪枝
粒子群算法在vivo笔试中出现的概率很高,因为它在图像匹配、相机参数标定等场景中很常用。粒子群模拟鸟群觅食行为,每个粒子有位置和速度,迭代更新时参考个体历史最优pbest和全局历史最优gbest。核心更新公式:
v[i] = w * v[i] + c1 * r1 * (pbest[i] - x[i]) + c2 * r2 * (gbest - x[i])
x[i] = x[i] + v[i]
其中w是惯性权重,c1、c2是学习因子,r1、r2是0到1之间的随机数。笔试如果考简答,把公式写出来,再说明初始化和迭代终止条件,基本就能拿满分。如果考代码,不需要写得特别复杂,画出基本框架就行。
模拟退火和剪枝算法也值得准备。模拟退火的核心是Metropolis准则,以一定概率接受更差的解,避免陷入局部最优。剪枝在搜索树中很常见,比如Alpha-Beta剪枝、DFS中的可行性剪枝和最优性剪枝。这些算法直接考代码的概率不大,但会在问答题中以“如何优化搜索效率”的形式出现。
2.5 机器学习与深度学习:KNN、聚类、强化学习都要懂一些
vivo算法岗笔试对机器学习的考察偏基础。KNN是重点,它的应用能力可以概括为三方面:分类、回归和异常检测。KNN分类通过多数投票决定类别,回归通过取k近邻均值预测连续值,异常检测则是利用样本到近邻的距离来判断离群点。KNN没有显式训练过程,属于懒惰学习,但预测时计算量大,样本维度高了还会面临“维度灾难”。
聚类的常见算法也要能说出区别:K-Means适合球形簇,DBSCAN能发现任意形状的簇且能处理噪声,层次聚类可以输出树状图。我遇到过一道选择题,给了一张散点图问适合用什么聚类算法,答案就是DBSCAN,因为图里有两个弧形簇和一堆噪声点。
深度学习部分,CNN的卷积层、池化层、全连接层的基本作用要能讲清楚。RNN处理序列数据,LSTM解决了RNN的长依赖问题。2024年的笔试越来越喜欢问大模型和端侧部署相关的问题,比如模型量化、剪枝蒸馏,这些最好也了解一点。强化学习的核心要素是状态、动作、奖励和策略,知道马尔可夫决策过程和Q-Learning的基本流程就够了。
2.6 工程算法速览:快速幂、BM25、PID、SOBEL、Rete
这块内容比较杂,但确实在各个行业的笔试里都出现过。快速幂是必须拿分的题,不要只会背模板,要理解二进制分解原理。BM25是搜索引擎常用的文本相关性算法,核心是词频、逆文档频率和文档长度归一化的结合。PID控制器在电源管理、电机控制里非常重要,基本原理是根据偏差的比例、积分、微分三项来计算输出。增量式PID的公式也要记一下,它计算的是控制量的增量,适合执行器带记忆的场合。
SOBEL算子和拉普拉斯算子都是图像锐化、边缘检测的基础算子。SOBEL通过对图像做水平和垂直方向的卷积,计算梯度幅值来检测边缘;拉普拉斯是二阶微分算子,对噪声敏感,实际用的时候通常会先做高斯平滑。Drools规则引擎里的Rete算法更偏后端,但笔试问算法匹配原理时,只要能说出“构建规则网络、共享条件节点、事实在节点间传递匹配”这个核心思路就够了。
3. 编程题实战套路:这几道题吃透,笔试稳了一半
3.1 手写快速幂:递归和迭代两种写法都要会
快速幂在很多题目里是优化关键,比如计算a的b次方再对mod取模。核心思想是把指数b拆成二进制,利用a^b = a^(2^k1) * a^(2^k2) * ...,从而把时间复杂度从O(b)降到O(log b)。
递归写法:
long long powMod(long long a, long long b, long long mod) { if (b == 0) return 1 % mod; long long half = powMod(a, b / 2, mod); half = half * half % mod; if (b % 2 == 1) half = half * (a % mod) % mod; return half; }迭代写法:
long long powMod(long long a, long long b, long long mod) { long long res = 1 % mod; a %= mod; while (b > 0) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res; }常见错误有三个:一是忘记处理b为0的情况;二是模运算时没有先对a取模,导致int溢出;三是递归写法里把“b是奇数”的判断写成了b % 2 == 0,结果全错。笔试现场如果时间紧,建议直接默写迭代版,因为它不会爆栈,也不用担心递归过深。
3.2 手写KMP完整实现:从next数组到匹配过程
KMP不是背代码就行的算法,要能边写边解释。这里给出一份完整的KMP匹配函数:
int kmp(const string& text, const string& pattern) { vector<int> next = buildNext(pattern); int n = text.size(), m = pattern.size(); int j = 0; for (int i = 0; i < n; i++) { while (j > 0 && text[i] != pattern[j]) j = next[j - 1]; if (text[i] == pattern[j]) j++; if (j == m) return i - m + 1; } return -1; }我笔试时曾在这道题上栽过跟头,原因是next数组的构建用的是“最长相等前后缀长度”,而匹配回退时用的是next[j-1],两边定义没统一。所以你一定要确认自己的代码里,buildNext返回的到底是什么含义,并保持一致。
3.3 现场模拟题:旋转数组最小值、二叉树层序遍历
我把两道代表性题目放在一起模拟一下。
旋转数组最小值:一个原本升序排列的数组,在某个点做了旋转,比如[4,5,6,7,0,1,2],要求找到最小值。最优解是二分查找,时间复杂度O(log n)。如果中间值小于右边界,说明最小值在左半部分,包括mid本身;否则在右半部分。
int findMin(vector<int>& nums) { int left = 0, right = nums.size() - 1; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] > nums[right]) left = mid + 1; else right = mid; } return nums[left]; }二叉树层序遍历:用队列做BFS,注意每层要先记录当前队列大小,再循环弹出,否则无法区分层级。
vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> res; if (!root) return res; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int sz = q.size(); vector<int> level; for (int i = 0; i < sz; i++) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } res.push_back(level); } return res; }这种题难度不大,但很考验代码基本功。我建议你把它们作为“必会题”反复练习,直到10分钟内能完整写出来。
4. 避坑清单与高效复习路线:实战踩坑后的经验总结
4.1 笔试现场最容易踩的坑
第一,选择题乱用“想当然”。比如排序算法稳定性,很多同学凭直觉以为快排是稳定的,实际上快排的partition过程中会交换相等元素,是不稳定的。这种题错一次就记住了,但笔试现场可没有让你错第二次的机会。
第二,问答题只写结论不写过程。我见过有人回答“KMP算法是通过预处理模式串来加速匹配”,然后就没有然后了。这样等于没答。至少要把next数组的定义写出来,再写匹配过程的基本步骤。笔试阅卷是按点给分,多写一个公式多拿一分。
第三,编程题不处理边界条件。字符串匹配题没考虑空串,二分题没处理数组长度为1,树题没处理空树。这些都是送分题变成送命题的典型。写代码前,先把输入范围读一遍,尤其是0、负数、极大值这些边界。
另外有个小提醒:很多同学平时接触vivo手机刷机、adb调试、fastboot指令包,对手机厂商的工程工具很熟悉,但算法岗笔试不考这些。不要把复习时间浪费在系统工具和刷机指令上,哪怕你对adb失效问题颇有心得,笔试它也不加分。刷题才是算法岗笔试的王道。
4.2 春招秋招通用的四轮复习路线
第一轮,数据结构基础:数组、链表、栈、队列、哈希表、二叉树、堆、图。目标是能手动实现二叉树的遍历、链表反转、用两个栈模拟队列,这些是后续所有算法的地基。
第二轮,算法思想专项:二分查找、双指针、滑动窗口、DFS、BFS、动态规划、贪心、回溯。这里推荐按专题刷题,不要乱序刷,否则很难形成体系。
第三轮,高频进阶算法:KMP、快速幂、Dijkstra、并查集、Trie树、拓扑排序。这些算法技巧性强,笔试也喜欢考,属于“练过就会,没练就废”的典型。
第四轮,行业场景拓展:如果你是投vivo这类手机厂商,尽量把图像算法(SOBEL、拉普拉斯锐化)、音频算法(重采样)、搜索排序算法(BM25)、控制算法(PID)都过一遍。不需要写完整实现,能理解原理、写出公式或描述流程即可。
4.3 常见报错与系统问题速查表
| 报错类型 | 可能原因 | 处理建议 |
|---|---|---|
| TLE(超时) | 暴力解法数据规模太大 | 换二分、哈希、双指针或DP优化 |
| MLE(超内存) | 数组开太大或使用了递归栈 | 改为滚动数组,减少辅助空间 |
| RE(运行时错误) | 数组越界、空指针、除零 | 检查循环边界和输入极端情况 |
| WA(答案错误) | 逻辑或边界条件有误 | 构造小样例手动跑一遍,加printf调试 |
| PE(格式错误) | 多输出了空格、换行等 | 严格按题目输出格式检查 |
笔试系统偶尔也会出幺蛾子。我在某次模拟测试时,发现明明本机跑得好好的代码,提交上去就是编译报错。后来发现是没选对编程语言版本,或者漏写了头文件。这里建议提交前务必检查语言版本和头文件,像#include <bits/stdc++.h>这种写法虽然方便,但在部分在线编译器上可能不支持,最好老老实实包含具体头文件。
4.4 关于复习资料与心态的实操心得
资料方面不需要贪多。一本《算法竞赛入门经典》加一个在线刷题平台就够,关键是把做过的题反复总结。我习惯每道题做一个“错因记录”,比如“忘记判断空队列”、“取模溢出”、“字符串下标越界”,笔试前翻一遍错题本,效果比刷十道新题还好。
心态也是重要变量。vivo笔试虽然覆盖广,但难度总体友好,只要平时多练,不用担心被某道偏题卡死。我考试时遇到一道陌生问答题,当时有点慌,后来想起复习过的粒子群算法框架,类比着写下来,居然也拿了不少分。笔试考的不只是你会不会,还有你面对不会的问题时,能不能冷静地把已知的东西组织起来。
我在实际准备过程中最大的体会是:这类笔试拼的从来不是奇技淫巧,而是稳定的基础输出。能把KMP的next数组推导清楚、能默写出快速幂、能讲明白粒子群的速度更新公式,再配合一轮系统的刷题训练,通过笔试的把握就会大很多。
如果你正在准备下一场笔试,最后再分享一个小技巧:复习时把每个高频算法都写在一张白纸上,从原理、公式、复杂度到适用场景,像面试一样默写一遍。能默写出来的才是真会,翻着书觉得“我懂了”的那种,大概率一到考场就露馅。