ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

哔哩哔哩算法岗笔试题复盘:从KMP到机器学习核心考点全解析

哔哩哔哩算法岗笔试题复盘:从KMP到机器学习核心考点全解析 1. 这场笔试的背景与整体判断1.1 2019年算法岗考察趋势2019年秋招并不是算法岗最好过的一年准确说从2018年下半年开始算法岗的竞争已经明显升温。原因也不复杂AI热度持续走高大量学生转算法方向但真正对口的坑位并没有同步增长。这种情况下笔试环节就成了筛人的第一道大闸题目质量普遍比前两年更“务实”。哔哩哔哩作为当时已经有一定用户体量的视频社区算法岗的笔试并不像有些大厂那样动不动就上极难图论题或LeetCode Hard压轴整体风格偏向“基础扎实就能过”但它的题面设计和实际业务结合得比较深。比如弹幕语义理解、视频推荐排序、用户增长相关的特征工程问题都会以选择题或问答题的形式出现。这其实比纯刷题更难准备因为你不仅要会写代码还得能说清楚“这个算法在业务里怎么用”。第三套笔试题我记得很清楚整体题量不大两个小时左右能写完但题与题之间的跨度不小。从数据结构、字符串匹配、图论遍历到机器学习基础、深度学习调参、系统设计问答题都有涉及属于典型的“什么都考一点但每一点都考到关键处”的风格。1.2 卷面结构与考点分布我根据自己的回忆和现场记录把这份卷子的结构拆成三大块第一部分选择题大约10道覆盖数据结构、排序算法稳定性、哈希冲突处理、递归复杂度计算、概率统计和机器学习基础概念。这部分拼的是基础功底基本没有太多思考时间。第二部分编程题3道左右涉及字符串处理KMP、链表操作、贪心/动态规划。这部分是拉开分差的关键。第三部分问答题/系统设计题考察推荐系统链路理解、特征工程思路、模型评估指标等内容。这部分的实际区分度比编程题还高因为很多人代码能过OJ但一落到“为什么这么设计”就说不清楚了。说实话哔哩哔哩这套题的风格在当时算比较友好的一档不像有些公司直接甩一道Hard级别的DP题让全场沉默。但它也没有简单到可以裸考上阵尤其是KMP的next数组题如果考前没有手推过三五个模式串现场很容易卡住。2. 核心算法题解析从读题到AC的实现思路2.1 字符串匹配与next数组KMP的基础功这套卷子里最醒目的编程题之一就是KMP相关的题目。题目给了一个模式串 p abacaba要求手写或推导其 next 数组。这里要注意不同教材对 next 数组的定义略有差别有的从0开始有的从-1开始所以做题前一定要先看清题目给的约定。我当时遇到的是最常见的一种定义next[i] 表示模式串前 i 个字符组成的子串中最长相同前后缀的长度。也就是说对于下标从0开始算的 next[i]它等于 p[0...i-1] 这个前缀子串里既是前缀又是后缀的最长子串长度。例如next[0] 通常初始化为 -1 或 0具体看题目约定对于 p abacaba它的前缀子串是 a、ab、aba、abac、abaca、abacab逐个计算最长相同前后缀长度。比如 aba 的前后缀公共部分是 a长度为1abac 没有公共前后缀长度为0abaca 同理为0abacab 的公共前后缀只有 ab长度为2。如果按 next[i] 表示“当前字符匹配失败时模式串指针应该回退到的位置”来算那“abacaba”的 next 数组算出来是 [-1, 0, 0, 1, 0, 1, 2, 3]最后一个 next[7] 是对整个字符串abacaba求最长公共前后缀得到3对应前缀 aba 和后缀 aba 相同。下面是一个可以直接跑的C版本的KMP匹配代码处理的是“在主串中查找模式串首次出现位置”的通用需求#include iostream #include vector #include string using namespace std; vectorint buildNext(const string p) { int m p.size(); vectorint next(m 1, 0); // next[i] 表示 p[0..i-1] 的最长相同前后缀长度 // 这里用 -1 作为哨兵方便统一处理 int j -1; next[0] -1; int i 0; while (i m) { if (j -1 || p[i] p[j]) { i; j; next[i] j; } else { j next[j]; } } return next; } int kmpSearch(const string s, const string p) { int n s.size(), m p.size(); vectorint next buildNext(p); int i 0, j 0; while (i n j m) { if (j -1 || s[i] p[j]) { i; j; } else { j next[j]; } } if (j m) return i - j; return -1; } int main() { string text abacabacaba; string pattern abacaba; cout kmpSearch(text, pattern) endl; // 输出 4 return 0; }这里我建议你在笔试现场先用小规模的字符串手工验证一遍 next 数组再写代码避免因为定义不一致导致整个匹配流程跑偏。这个坑我见过太多人踩了——不是不会 KMP而是被 next 数组的不同定义绕晕。2.2 链表反转变体不只是背模板除了 KMP这套题里还有一道链表的题目但并不是简单的“反转整个链表”。我记得题目描述里加了一个限制条件要求只反转链表中从第 m 个节点到第 n 个节点之间的部分其余部分保持原样。这种题在LeetCode上对应的是“Reverse Linked List II”属于常见题但笔试现场能一次写对的人并不多。主要难点在于边界处理。你需要记录 m 位置的前一个节点 pre反转完 m 到 n 这段之后把 pre 的 next 指向反转后的头节点同时把反转段的尾节点接到原来的 n1 节点上。如果不设虚拟头节点m 1 的时候会非常容易出错因为头节点的前一个节点不存在。我当时用的做法是先加一个虚拟头节点 dummy让代码路径统一然后做一次局部反转。核心伪代码如下ListNode* reverseBetween(ListNode* head, int m, int n) { ListNode* dummy new ListNode(-1); dummy-next head; ListNode* pre dummy; for (int i 1; i m; i) pre pre-next; ListNode* cur pre-next; for (int i m; i n; i) { ListNode* nxt cur-next; cur-next nxt-next; nxt-next pre-next; pre-next nxt; } return dummy-next; }这段代码的思路是头插法每次把 cur 的下一个节点摘下来插入到 pre 的后面不断循环直到反转区间完成。实际跑下来这个写法虽然看着绕但胜在不容易把指针搞丢。就是要注意循环次数是 n - m 而不是 n - m 1这个细节容易搞混。2.3 贪心与区间问题高频考点这套题的编程题部分还有一道区间贪心的题目类似“给定若干会议时间求最多能参加多少个会议”。这种题的核心是先按结束时间排序然后依次选择“结束最早且不与前一个已选区间重叠”的区间。时间复杂度 O(n log n)空间复杂度 O(1)如果只排序的话。为什么一定要按结束时间排序而不是按开始时间或区间长度排序我举个例子你就能理解了假设有三个区间 [1, 4]、[2, 3]、[3, 5]按开始时间排序会先选 [1, 4]那后面的 [2, 3] 和 [3, 5] 都不能选最多只能选一个但按结束时间排序先选 [2, 3]再选 [3, 5]就能选两个。如果按区间长度排序最短的是 [2, 3] 和 [3, 5]长度都为2二选一后又只能选一个。所以“结束最早”才是正确的贪心策略。2.4 动态规划模型从状态定义到转移方程动态规划题在这套卷子里也有出现但并非压轴难题而是比较经典的“打家劫舍”变体。题目大意是一排房屋每家有一定金额相邻两家不能同时被偷求能偷到的最大金额。这个题的状态定义非常线性设 dp[i] 表示偷到第 i 家时的最大收益那么转移方程为dp[i] max(dp[i-1], dp[i-2] nums[i])边界条件 dp[0] nums[0]dp[1] max(nums[0], nums[1])。这题说白了就是考你有没有总结过“线性DP”的套路。只要状态定义清楚了代码五分钟就能写完。但我要多说一句笔试最后一道题往往会给一个看似复杂的DP场景比如二维格子、带障碍物、带最小路径和等。这时候不要急着上手写循环先在草稿纸上把状态转移图画出来确认“我从哪里来”和“我到哪里去”这两个方向再动手写代码正确率会高很多。3. 算法之外的“隐藏题”工程与机器学习基础3.1 机器学习/深度学习考点虽然这是算法岗的笔试但纯考算法的题目只占了不到一半剩下的更多是“你会不会在实际工作中用这些算法”。比如我记得有一道选择题是关于过拟合的解决方式选项包括增加训练数据、正则化、Dropout、增加模型深度。这题本身不难但很多人会漏选“增加模型深度”因为直觉上总觉得模型越深能力越强应该能拟合得更好。但实际上模型深度增加通常会增加参数规模反而更容易过拟合所以它不能作为解决过拟合的手段。还有一道题考的是 BatchNorm 的作用。这个知识点如果你只是背过“归一化”三个字很容易答错。BatchNorm 的核心逻辑是对每个 mini-batch 在通道维度上做标准化让数据分布稳定下来从而允许使用更大的学习率同时缓解梯度消失/梯度爆炸问题。它还附带轻微的正则化效果因为 batch 统计量的随机性会给网络带来一点噪声。所以正确答案一般会包含“缓解内部协变量偏移”和“加速收敛”两个方面。另外深度学习部分还涉及 CNN 的感受野计算。题目给了一个三层卷积网络卷积核大小分别为 3×3、3×3、3×3步长都是1padding 都是1问最后一层输出的感受野是多少。这里你不要只看公式要理解“感受野”的本质是输出特征图上每个像素对应回输入图像的区域大小。三层 3×3 卷积叠加感受野为 7×7。如果你对这部分不熟建议考前专门花半小时把“卷积核大小、步长、padding 与感受野”的关系推导一遍这是笔试高频考点。3.2 计算机基础与工程问题这套笔试题里还混入了一些计算机基础题比如 TCP 三次握手、进程和线程的区别、数据库索引为什么用 B 树等。这些题本身不难但很多算法方向的同学在准备时只顾着刷 LeetCode把基础知识完全丢了结果笔试时反而在这一块丢分。我印象最深的是有一道问答题让描述“如何判断一个用户是否为平台的忠实用户”并给出特征和模型方案。这种开放式问题考察的是工程直觉没有标准答案。我当时写的是先定义忠实用户的行为口径比如近30天内登录天数≥15天、视频播放时长≥一定阈值、有至少1次互动行为再围绕这个口径构造特征比如活跃度、内容消费多样性、互动频率、分享次数等最后用 GBDT 或逻辑回归建模并配合规则兜底。这种题的关键不是模型多高级而是你能不能把“业务问题”翻译成“机器学习问题”。4. 代码运行的现场细节边界、复杂度与调试4.1 边界条件与特殊输入笔试环境中最常见的翻车点不是不会写算法而是边界条件没有考虑全。比如链表的 m 等于 n 时按原逻辑反转一个节点结果应该保持不变KMP 匹配时主串为空或者模式串为空程序不能崩区间合并时输入为空数组结果应该返回空数组。我建议你养成一个习惯写任何算法题先写完正常逻辑然后立刻在心里过一遍“空输入、单元素输入、全相同元素输入、最大输入规模”这四种情况。这个习惯在面试手撕代码时同样适用而且非常加分。我当时在这套题上就吃过亏。有一道题是关于二叉树层序遍历的题目没有明确说“如果输入为空树应该输出什么”我默认输出空数组。但有些 OJ 系统的判题脚本期望的是输出“[]”还是什么都不输出并不统一。如果你的代码做了特殊判断结果反而可能被误判。当然这种情况不多但仍提醒你注意题目下方有没有“说明”部分的细节。4.2 复杂度的经验判断笔试做题时一定要训练自己对复杂度的直觉。一般来说如果 n 的范围是 10^5那你的算法复杂度最多是 O(n log n)不能再高了如果 n 20那大概率可以暴力枚举或状压DP如果 n 1000O(n^2) 可能勉强能过但也要小心常数优化。比如 KMP 题很多人也能用暴力匹配实现但一旦主串长度到 10^6暴力 O(n*m) 就会超时。所以看到字符串匹配题第一反应不应该是直接写两层循环而是想一下“有没有可能卡我复杂度”。同理链表题如果允许 O(n) 空间你可以用栈来辅助但如果题目明确要求 O(1) 空间那你就必须用指针操作。4.3 笔试环境与语言选择哔哩哔哩这套题使用牛客网的笔试系统编程题需要自己处理输入输出格式。这里有一个容易被忽视的点牛客网的输入可能有多组测试用例尤其是“循环读取到 EOF”的情况。如果你只读了一组数据就输出结果大概率会“通过率 0%”。再就是语言选择。我个人主写 C因为 STL 里的容器和算法库能省不少事而且执行效率高。但如果你对 Python 更熟练用 Python 也完全可以。关键不是语言本身而是你能否在半小时内把一道题从思路变成无 bug 的代码。如果你决定用 Python建议提前熟悉 sys.stdin.read() 这种批量读取方式避免因为输入解析浪费时间。5. 复盘与刷题建议从“会做”到“做得稳”5.1 按专题刷题而不是按难度刷很多人刷题喜欢从 Easy 到 Hard 一路刷过来但实际准备秋招笔试我更推荐按专题推进。比如花三天专门搞字符串匹配KMP、BM、字符串哈希再花三天专门搞链表操作反转、删除、合并、找环再花三天搞线性DP。这样做的原因是笔试题目大概率不是单一知识点的裸题而是多个基础概念组合起来的“缝合题”按专题刷才能帮你形成“知识块”的直觉。以链表为例你可以把“反转链表”“两两交换链表节点”“每K个一组翻转链表”“链表求和”“重排链表”放一起刷刷完之后你会明显感觉到反转类操作的套路来来回回就那几种头插、尾插、双指针。5.2 边刷边总结“为什么这样做”我见过很多同学刷题量很大但碰到原题仍然写不出来原因就是只记住了代码没有记住思路。比如 KMP 的核心是“利用已经匹配的字符信息”所以它在失配时不是从头再来而是跳到最长相同前后缀的位置继续匹配。这个思想你可以迁移到“数组中的前缀函数”“字符串哈希对比”等场景中。我建议准备一个“一题一记”的文档每道题记录三点题目在考哪个知识点、我的思考卡在了哪一步、标准解法比我好在哪里。笔试前翻一遍这个文档远比再刷一百道题有用。5.3 模拟笔试训练最后再提一个非常实用的方法考前至少做两次完整的模拟笔试。设好两小时倒计时用牛客或LeetCode的模拟环境不查资料、不中途暂停完全按照真实笔试节奏来。这样做的好处有两个一是让你学会时间分配知道哪些题该放弃、哪些题该死磕二是让你适应“在屏幕前写代码”的状态因为笔试和平时在 IDE 里写代码的体验完全不同比如没有自动补全、没有 lint 提示、没有断点调试。我第一次模拟笔试时第二道编程题因为环境不熟悉浪费了20分钟导致最后一道简单贪心题都没来得及写。那次教训特别深刻从那以后每次笔试前我都会做一次完整模拟。从这套哔哩哔哩笔试题往回看2019年算法岗笔试的主流风向已经很明确了代码基本功要硬机器学习基础要熟业务理解要有三者缺一不可。单纯刷题能帮你过笔试但只有真正理解了算法背后的原理和适用场景才能在后续的面试中继续走下去。如果你正在准备算法岗希望这篇复盘能帮你少走一些弯路。
返回列表