2019年秋季那波校招,B站是很多同学盯了很久的目标。喜欢追番、刷弹幕,想着有一天能去写视频网站的后端代码,把爱好直接变成工作。我身边好几个朋友都在那年投过B站的开发岗,回来之后把面试遇到的编程题汇总成了一份文档。后来我自己也把这份合集从头刷了一遍,发现即使是隔了几年再拿出来看,里面的题依然很有嚼头。原因是B站技术面的风格一直比较稳,考的东西不追求偏怪难,而是把计算机基础、算法和工程意识揉在同一套题里。
这份2019秋招编程题合集,整理下来大约二三十道题,覆盖了数组、字符串、链表、二叉树、动态规划,还有少量系统设计题。它适合三类人:第一类是正在准备校招和实习的应届生,想提前感受B站面试的难度和考法;第二类是工作几年想跳槽的工程师,拿它当算法手感恢复的燃料;第三类是单纯想练基本功的人,把题目当题库做,顺便观察视频网站业务的工程诉求。无论哪一种,只要你能把这份题吃透,收获的绝对不只是“会做几道题”,而是对面试官出题逻辑的把握。
1. 为什么一份2019年的秋招题现在还值得翻
1.1 秋招真题的价值不在“旧”,而在“稳”
很多人一听是2019年的题,第一反应是“都过去这么久了,题型肯定变了吧”。实际上,技术面试的底层考察点更新速度远比技术栈迭代慢得多。2019年考滑动窗口、最长上升子序列、二分查找边界,现在校招依然在考,只是包装成不同的故事背景。B站这家公司的题目更有代表性,因为它的业务场景包含视频上传、弹幕、推荐、搜索,这些场景天然适合出算法题和工程题,所以题目内容和日常工作贴合得非常紧。
我刷这份题的时候有个明显感受:题目不搞“脑筋急转弯”,也没有故意加一堆刁钻限制来卡人。它更像是在考察一个工程师面对真实问题时的思考路径——能不能先给暴力解,能不能分析出瓶颈,能不能在提示下一步步优化到最优解。这种风格恰恰是校招面试里最值得提前适应的,因为你平时在OJ上刷题,往往直接奔着最优解去,但现场面试要求的是“思考过程可见”。
所以这份合集不是一个过期的题库,而是一套稳定的能力标尺。它帮你检验的,是你对基础数据结构和经典算法的熟练度,以及在压力下能不能把思路讲清楚、把代码写对。
1.2 这份合集适合谁去刷
先说应届生。校招准备最怕的就是“刷题方向跑偏”,花大量时间钻研冷门数据结构,结果面试官问的都是高频基础题。用这份合集做定位,你能快速知道B站这类视频互联网公司喜欢考什么,以及每题大概是什么难度。建议是:先独立做一遍,再对照题解复盘,而不是上来就看答案。
再说想跳槽的工程师。工作几年之后,很多人算法手感明显退化,不是说不会,而是手生。拿这份题当“恢复训练”很合适,题目量不大,难度梯度合理,每天两三道,一周左右就能找回状态。尤其是里面的工程场景题,对社招面试更有参考价值,因为社招本来就更看重系统设计能力和业务理解。
如果你只是单纯想练基本功,这份合集同样值得刷。它能帮你把字符串、数组、动态规划、二叉树这些核心板块串起来,形成一套完整的解题框架。我甚至建议你把它当“自测卷”用,限定两小时做完,检验自己哪个板块最薄弱。
2. 题型全景与考察重点拆解
2.1 从题型分布看B站技术偏好
我根据当年收集到的面经和自己的刷题记录,把这份合集中的题目按类型做了个粗略统计。虽然每一年题目会有微调,但整体分布相当稳定。
| 题目类型 | 题量占比 | 常见考点 | 我的判断 |
|---|---|---|---|
| 字符串处理 | 20%左右 | 滑动窗口、子串匹配、字符串模拟 | 出镜率最高,几乎必考 |
| 数组与二分 | 20%左右 | 双指针、二分边界、前缀和 | 性价比极高,容易拿满分 |
| 链表与栈 | 15%左右 | 反转链表、单调栈、LRU思想 | 考察代码基本功 |
| 二叉树 | 10%左右 | 层序遍历、最近公共祖先、路径问题 | 中规中矩,但必须熟练 |
| 动态规划 | 15%左右 | 最长上升子序列、编辑距离、背包变体 | 拉开差距的核心板块 |
| 工程场景设计 | 10%左右 | 限流、断点续传、缓存策略 | 结合业务,考察综合能力 |
| 其他杂项 | 10%左右 | 排序、位运算、随机数 | 作为调剂和加题 |
这个分布透露出的信息挺直接:B站不喜欢考特别复杂的图论和高级数据结构,而是把重心放在“工程师日常最常用的算法”上。字符串和数组为什么占比高?因为视频网站到处都是文本处理、搜索词匹配、播放状态判断,这些场景天然和字符串、数组强相关。动态规划占比不低,是因为它最能考察一个人的逻辑推导能力和状态抽象能力。
2.2 各模块知识点权重分析
如果按“是否必须拿分”来给这些知识点排个序,我的建议是这样的:
第一优先级:滑动窗口、双指针、二分查找、链表反转、层序遍历。这些题属于“背也要背熟”的品类,因为它们解法固定、套路清晰,只要练过就一定能写对。现场如果栽在这里,面试官对你的印象会大打折扣。
第二优先级:动态规划、单调栈、前缀和、最近公共祖先。这些题需要一定的分析能力,但题型有限,可以在短期内有针对性地突破。尤其是动态规划,建议把常见的几种状态定义方式吃透,比如“以i结尾”“前i个元素”“区间[i,j]”,大部分题都能往这几个模子里套。
第三优先级:系统设计、工程场景题。这类题没有标准答案,但反而最容易提前准备。你只需要理解常见业务的通用方案,再结合B站的特点说出来就行。后面第四章我会专门展开讲弹幕限流和断点续传这两个高频场景。
3. 算法题精讲:三道必会的核心题
3.1 无重复字符的最长连续子串——滑动窗口的标准姿势
这道题在2019年B站后端岗的现场面试里出现过,基本可以算视频网站面试的“保留曲目”。题目描述非常简洁:给定一个字符串s,找出其中不含有重复字符的最长连续子串的长度。
比如s = "abcabcbb",答案是3,对应的子串是"abc"。看起来简单,但想一次性写对,需要想清楚左右边界怎么移动。
最笨的办法是枚举所有子串,再逐个检查是否有重复字符,时间复杂度O(n^3),基本属于“说出来会被立刻打断”的方案。稍微好一点的是用两个for循环枚举起点和终点,借助一个set判断重复,复杂度O(n^2)。但面试官真正想听的优化方案,是滑动窗口,把时间复杂度压到O(n)。
用Python写出来是这样的:
def lengthOfLongestSubstring(s: str) -> int: last = {} left = 0 res = 0 for right, ch in enumerate(s): if ch in last and last[ch] >= left: left = last[ch] + 1 last[ch] = right res = max(res, right - left + 1) return res核心逻辑只有两句话:遇到重复字符时,把左边界跳到该字符上一次出现位置的下一个;然后更新当前字符的最新位置,同时维护最长长度。很多初学者会问:为什么判断条件是last[ch] >= left,而不是last[ch]存在就行?因为如果这个字符上一次出现的位置已经在当前窗口左边了,说明它不在窗口内,不需要移动左边界。这个细节最容易踩坑,不加上会报错。
我当年第一次写这道题,就是把left直接改成last[ch] + 1,结果窗口直接跳过了正确答案。后来才意识到,必须保证“上一次出现位置在当前窗口内”,才需要收缩左边界。这道题测试时还要注意空字符串和单字符这两个极端情况,空串返回0,单字符返回1。
3.2 最长上升子序列——从O(n²)到O(n log n)
最长上升子序列(LIS)是动态规划里的经典题,B站2019秋招也考过。题目是给定一个无序整数数组nums,求最长的严格递增子序列长度。注意是子序列,不是连续子数组,所以元素可以不连续。
比如nums = [10,9,2,5,3,7,101,18],答案是4,对应子序列[2,3,7,101]或者[2,5,7,101]。
这道题有两条路线。第一条是朴素动态规划,定义dp[i]为以nums[i]结尾的最长上升子序列长度。初始化时每个dp[i]都至少是1,因为单个元素本身可以看作长度1的上升子序列。然后遍历i之前的所有j,只要nums[j] < nums[i],就用dp[j] + 1去更新dp[i]。最后答案就是dp数组里的最大值。
def lengthOfLIS(nums): n = len(nums) if n == 0: return 0 dp = [1] * n for i in range(n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp)这个写法时间复杂度O(n^2),空间复杂度O(n)。面试时先给出这个版本,能让面试官看到你的基础DP能力,然后再提优化。第二条路线是贪心加二分,把时间复杂度降到O(n log n)。这里维护一个tails数组,tails[i]表示长度为i+1的上升子序列中,末尾元素的最小值。遍历每个数字x,用二分查找在tails里找到第一个大于等于x的位置,替换掉它;如果x比tails末尾还大,就追加到末尾。最后tails的长度就是答案。
import bisect def lengthOfLIS(nums): tails = [] for x in nums: pos = bisect.bisect_left(tails, x) if pos == len(tails): tails.append(x) else: tails[pos] = x return len(tails)这里有个很多人容易搞混的点:tails数组里存的并不是真正的最长上升子序列,它只是一个“末尾最小值”的维护结构。比如nums=[4,5,6,1,2,3],tails最终会是[1,2,3],长度是3,但真实的最长上升子序列可以是[4,5,6],长度也是3。你只能用tails的长度,不能用它的内容。另外,题目要求严格递增,所以用bisect_left;如果改成非递减,就不得不换成bisect_right,这是非常隐蔽的一个坑。
3.3 旋转数组最小值——二分边界怎么卡才不翻车
旋转数组最小值这道题,B站2019秋招里也出现过。题目说,一个升序排列的数组,把前面若干元素搬到末尾,形成旋转数组。要求找到数组中的最小元素。比如[4,5,6,7,0,1,2],输出0,原数组没有重复元素。
这题看似是二分,但和普通二分找目标值不同,它找的是“分界点”和“最小值”。标准解法是维护left和right,每次取mid,比较nums[mid]和nums[right]的大小关系。如果nums[mid] > nums[right],说明最小值在右侧区间,left = mid + 1;否则说明最小值在左侧区间或就是mid,right = mid。循环结束条件用left < right,最后返回nums[left]。
def findMin(nums): left, right = 0, len(nums) - 1 while left < right: mid = (left + right) // 2 if nums[mid] > nums[right]: left = mid + 1 else: right = mid return nums[left]为什么和nums[right]比较,而不是和nums[left]比较?这是这道题最关键的考点。如果nums[mid] > nums[left],并不能确定最小值在哪边。比如[4,5,6,7,0,1,2],mid对应的是6,nums[mid] > nums[left](6大于4),但最小值在右边。所以和left比较很容易被边界条件误导。而nums[right]在升序数组中一定是“区间内最大可能的锚点”,只有当右半段发生了旋转,nums[mid]才会大于nums[right],这是一种唯一且确定的条件。
另外补充一点:如果允许重复元素,比如[2,2,2,0,1,2]这种,当nums[mid] == nums[right]时,怎么处理?做法是让right -= 1,一个一个地收缩右边界,让左指针逼近最小值。代价是极端情况下时间复杂度退化为O(n)。面试时如果被追问重复元素的情况,能答出这层基本就过关了。
4. 工程场景题:从B站业务反推设计题
4.1 弹幕高频写入如何限流
工程场景题在B站2019秋招里虽然占比不算高,但一旦出现,很容易和具体业务绑定。最典型的一道是:某热门视频刚上线,弹幕量瞬间暴涨,弹幕网关该怎么设计限流,才能保证服务不挂。
这个题没有唯一答案,但考查的核心点是限流算法和分布式场景意识。我推荐按下面这个层次来回答:
单机层面,可以先用令牌桶或漏桶算法。令牌桶的实现思路是:系统以固定速率往桶里放令牌,每个请求需要取走一个令牌才能被放行,桶满则丢弃令牌。突发流量时,只要桶里有令牌就能通过,从而允许一定的突刺,同时整体速率又是可控的。漏桶则是将请求排队,以固定速率处理,擅长平滑流量,但对突发流量的容忍度低。两者选哪个,取决于产品要“保吞吐”还是“保平滑”。
由于弹幕网关通常不止一台机器,单机限流远远不够,必须考虑分布式限流。常见做法是用Redis + Lua脚本实现原子计数。比如滑动窗口或固定窗口计数器,每次请求执行一段Lua脚本,判断当前窗口内的请求数是否超过阈值。Lua脚本的好处是原子性,多个并发请求不会出现“检查完还没来得及加一,另一个请求也通过了”的超卖问题。伪代码大概是这样的思路:
local key = KEYS[1] local limit = tonumber(ARGV[1]) local current = tonumber(redis.call('GET', key) or '0') if current >= limit then return 0 else redis.call('INCR', key) redis.call('EXPIRE', key, 60) return 1 end答到这里,面试官一般会顺着追问“限流之后流量去哪了”。这时候要给出削峰填谷的思路:被限流器拦下来的弹幕,可以进入消息队列,由下游消费者按固定速率消费并写入存储。这样即使瞬时弹幕量是平时的一百倍,数据库也不会被打爆。最后再补一句降级策略:当队列积压到一定水位时,优先丢弃明显重复或低价值的弹幕,保证核心互动体验稳定。这套回答讲下来,覆盖面就很完整了。
4.2 断点续传方案如何设计
B站是视频网站,用户上传视频内容是一个非常核心的场景。网络不稳定导致上传中断,要求支持断点续传,这也是当年出现过的工程题。
我的回答框架分四层。第一层,分块上传。前端在上传前把文件切成固定大小的分片,比如每片4MB,每个分片独立上传。这样中断后只需要从失败的那一片开始,而不是整个文件重传。第二层,状态记录。后端需要记录每个分片的上传状态,可以用一张数据表或Redis,Key是上传会话ID,Value是已上传分片的编号列表。前端每次上传前,先向后端查询哪些分片已经上传成功,跳过这些分片。
第三层,分片校验。每个分片上传完成后,服务端返回该分片的MD5或CRC32值,前端比对结果确认是否成功。全部上传完成后,后端再按分片顺序合并文件。合并时要注意磁盘空间和文件完整性校验,通常会对整个文件再做一次MD5或者使用记录总大小的方式,确认没有缺失分片。第四层,断点恢复。当网络恢复或用户重新打开页面时,上传组件重新发起查询,拿到已上传分片列表,从下一个分片继续。如果有分片损坏或没上传完整,就只重传那一片。
设计里能体现“业务理解”的点是:要聊到视频文件通常很大的特点,以及转码流程对文件完整性的要求。如果上传的是原始视频文件,分片合并后还要先校验再进转码队列,避免转码到一半发现文件损坏,浪费大量计算资源。答出这个层次,面试官会认为你真的思考过视频上传链路,而不是只会背概念。
5. 刷题复盘的路线图
5.1 三个月冲刺节奏
如果你现在离秋招还有大约三个月,这份合集可以按三阶段来用。第一个月,主攻基础数据结构。每天安排2到3道数组、字符串、链表、栈和二叉树题目,把每一类题型的固定套路练熟。这个阶段不追求难题,但要确保每道题都能独立写对,并且能说出时间复杂度和空间复杂度。
第二个月,专题突破算法重点。动态规划、二分、滑动窗口、双指针,这四个专题是B站这类公司的高频考区。每天做一个专题,建议配合同类题目集中练习。比如今天专做动态规划,就连续做五道不同状态定义方式的DP题,而不是一天换一个知识点。集中练习的好处是,你能在短时间内建立“这类题长什么样”的直觉。
第三个月,进入模拟面试阶段。拿合集中的题目限定45分钟一题完整走一遍:读题、确认边界条件、说暴力解、再优化、手写代码、跑测试用例。这个过程要严格模拟现场,不能一边写一边查资料。我推荐的节奏是:上午一套完整模拟,下午复盘错题,晚上补弱项。每周再做一次全量回顾,把每个专题的解题套路写在纸上,能默写出来才算真的掌握。
5.2 每道题都应该有的三遍复盘法
很多同学刷题有一个通病:做完一道题,看一眼AC了就过,第二天全忘光。我建议把每一道题至少做三遍。第一遍,限时独立完成。如果45分钟没思路,直接看题解,不要硬耗。看完题解之后,必须关掉答案,自己重新写一遍代码。第二遍,隔一天再独立做。这时候你对题目的记忆已经在消退,能做出来才说明你真正理解了解法,而不是背下了代码。第三遍,隔三天后只做“思路复述”。在纸上写下这道题的解法关键词、时间复杂度、边界条件,不看代码,直到能清楚讲给一个虚拟面试官听。
这个方法看起来很笨,但坚持下来效果非常好。我刷这份合集中的动态规划题时,第一遍能写出来的不到一半,但三遍之后,几乎所有题都能在15分钟之内出思路。不要贪快,每周严格按节奏复盘,比打鸡血式刷一百道新题更有用。
6. 现场答题技巧与高频翻车点
6.1 时间分配和沟通顺序
现场写代码和平时刷题完全不同,你面对的是面试官,而不是屏幕上的题目描述。很多基础不错的应届生,因为没掌握表达节奏,导致明明会做也被刷。我总结了一个比较稳的答题顺序。
拿到题后,先花1到2分钟把题目复述一遍,同时主动确认边界条件。比如:数组可能是空吗?字符串包含哪些字符?数据规模大概多大?有重复元素吗?这些信息直接决定算法选型。比如数据规模到10^5,O(n^2)大概率超时,你就要优先想O(n log n)的方案。
然后,先讲暴力解。哪怕你知道更优解,也建议先把暴力思路一两句话说完,再过渡到优化方案。这样面试官能看到你“先保证正确,再追求效率”的工程思维。如果你一上来就写最优解,一旦卡壳,连兜底方案都没有。讲完思路之后,再动手写代码。写之前把时间复杂度和空间复杂度说清楚,写的过程中边写边解释关键变量。写完以后,主动提出跑一个测试样例,逐步检查。
整体时间分配建议:读题2分钟,思考5分钟,讲述思路2分钟,写码15分钟,测试和优化5分钟。大部分算法题控制在30分钟内完成比较理想。如果超过15分钟还没想到优化思路,就直接把暴力解写出来,至少能保证有产出。
6.2 常见问题速查表
刷题和面试反复踩坑之后,我整理了下面这张速查表,基本覆盖了算法面试里最高频的翻车点。
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| 滑动窗口left跳过头 | 误把重复字符上一次出现位置直接当left | 加上“上一次位置 >= 当前left”的判断 |
| DP数组初始值全是0 | 没有意识到单个元素本身是合法子序列 | 子序列类DP初始值大多设为1 |
| 二分陷入死循环 | right更新写成mid-1,缩小了本该包含答案的区间 | “找最小值”场景right = mid,条件用left < right |
| 递归层数过深导致栈溢出 | 二叉树深度大,递归压栈超限 | 改成迭代写法,或显式用栈模拟 |
| 写代码前没确认数据规模 | 误用O(n^2)方案导致超时 | 先问规模,再定复杂度级别 |
| 现场一紧张忘了API | 对语言内置库不熟 | 提前记好常用API,如Python的bisect、collections.deque |
| 只会背模板,无法解释思路 | 平时刷题靠记忆,没有理解原理 | 每道题强迫自己口头复述一遍解法 |
| 链表题忘记处理头节点 | 链表操作边界没想清楚 | 统一引入dummy头部节点,简化边界判断 |
其中“链表题忘记处理头节点”属于我见过最可惜的翻车。明明会做反转链表,就因为没加dummy节点,最后循环条件写错,整道题崩盘。这类问题靠天赋解决不了,只靠大量练习,把边界条件变成肌肉记忆。
从复现角度来看,我特别建议你把每道题的核心代码摘出来,按专题分类整理,形成自己的题典。考试前看题典里自己总结的易错点,比临时翻题解效率高得多。准备校招的冲刺阶段,时间比什么都珍贵,学会做减法、抓高频点,才能把努力变成分数。
我个人在实际准备过程中还有一个体会:刷这份合集,不只是为了通过某一场面试。里面的每一道题都像一面镜子,能照出你代码能力的短板。你把这份题刷透之后,再回头看日常项目里的代码,思考方式会有明显变化——遇到问题先想边界条件,再做复杂度评估,然后才动手实现。这个习惯,远比多背几道题更有价值。