2024年春招,我投了腾讯音乐的Java后端岗。简历投出去一周左右,收到了第一批笔试通知。说实话,点开邮件的时候心里还挺平静的,因为知道腾讯音乐的笔试向来口碑稳定——不搞偏题怪题,但基础考察极其扎实,对代码实现能力的要求也比较实在。真正坐到电脑前,打开考试系统的那一刻,才意识到这场笔试的核心逻辑:不是要你做出所有题,而是看你在限时压力下,能不能把会做的题稳定拿到分。
这篇就围绕我这场笔试题型、踩坑细节和对春招节奏的一些看法做个完整复盘。如果你正准备投大厂技术岗,或者对腾讯音乐这类业务型互联网公司的笔试风格好奇,这篇值得看完。我会尽量还原考场上的真实情况,包括选择题的考察方向、三道算法题的完整思路,以及那些只有上了考场才发现的坑。
1. 笔试前半场:选择题的考察方向与得分要点
腾讯音乐的笔试用的是牛客网系统,技术岗题目构成很固定:选择题加编程题,总分不一定公布,但每个部分的用时自己控制。我当时拿到的卷子是17道选择题加3道编程题,总时长90分钟。这个时长设置很有意思——看似宽裕,实际上选择题如果每题花超过2分钟,后面的编程题时间就会非常紧张。
先说说选择题。这部分覆盖范围很广,从数据结构、操作系统、计算机网络到数据库、Linux命令、Java/C++语言特性都有涉及。整体难度中等偏上,不完全是那种看一眼就知道答案的送分题,很多题目需要动笔比划一下才能确定。
1.1 数据结构与算法题:平衡树和哈希是高频点
选择题里数据结构占比最大,差不多有5-6道。我印象比较深的一道是关于平衡二叉树调整的:给出一棵插入过程中失衡的AVL树,问旋转之后变成什么形状。这道题考的不只是四种旋转(LL、RR、LR、RL)的名字,更需要在草稿纸上把每个节点的平衡因子算一遍。我的建议是别在脑子里转,动手画,很快就出来了。
还有一道哈希表相关的题,给定了哈希函数和冲突解决方式(链地址法),要求计算平均查找长度。这种题没什么技巧,老老实实把每个关键字散列到对应位置,画出链表再算。题目本身不难,但特别容易被"考点在链地址法"这个预期带偏——真正的考点其实是散列函数对数据分布的影响。
另一个容易丢分的是B+树。腾讯音乐这类公司,数据库相关的场景非常多,B+树的叶子节点存储结构、查找路径、范围查询优势,这些考点几乎是必出现。选择题里问的是向B+树插入一个关键字后,叶子节点分裂的细节。这种题如果不熟悉定义,很容易选错。
1.2 操作系统与网络的几道"老朋友"
操作系统的题目不算多,大约3道,但很典型。考了进程和线程的对比——这个属于送分题,理解了就选得出来。还有一道关于死锁的,四个必要条件里选择一个不属于的,这个也简单。真正有意思的是一道关于虚拟内存的题:给定页面访问序列和物理块数,用LRU算法算缺页次数。这题我在草稿纸上画了大概两分钟才确定答案,因为访问序列里有两个元素的访问顺序很容易被忽略。
网络部分有两道,一道TCP三次握手(问第二次握手发送的报文标志位),一道HTTP状态码(给出几个状态码问哪个表示重定向)。都是基础问题,但如果复习范围只盯着HTTP/1.1而忘了状态码分类,还是会卡一下。其实这类题反复考,考察的就是"你脑子里有没有一张清晰的知识地图",而不是死记某个数字。
1.3 数据库与Linux:业务画像很鲜明
这次笔试明显偏业务向,数据库相关的选择题出了两三道,包括索引失效的场景判断、事务隔离级别和锁机制。索引失效那道题我最喜欢:给了一个复合索引(a, b, c),然后给了四个查询条件组合,问哪一个会走索引。这属于经典的"最左前缀原则",但题目里混了一个范围查询,一旦b是范围查询,c那部分索引就用不上。这种题如果不仔细分析查询条件顺序,很容易错。
Linux题目比较简单,考的是常用命令的执行结果,比如在某个目录下找出最近修改的文件,用find还是grep还是awk。这其实不是考命令本身,而是考你对命令适用场景的理解。建议准备阶段多动手敲,别只看文档。
1.4 语言特性题:多语言混合出现的应对策略
腾讯音乐笔试的语言题不限定单一语言,Java、C++、Python都有可能出现。我当时遇到的有Java的HashMap在JDK 8中红黑树化的阈值(链表长度达到8且数组长度达到64),也有C++的虚函数表和构造函数调用顺序。如果你只熟一门语言,这些题也不至于完全不会,因为它们实际上考的还是通用概念——哈希冲突解决、继承与多态。
这里就涉及到一个小策略:遇到语言题不要慌,先看它在考什么底层机制,再用自己熟悉的语言知识去对号入座。比如C++的虚函数题,本质上考的是运行时多态的实现方式,和你学的Java接口实现是一回事。
2. 编程题复盘:从业务场景到算法本质的抽丝剥茧
编程题是这场笔试的重头戏,3道题每道20-30分,合计60分以上。拿到题目第一感觉是:出题人很懂音乐业务,题目都披着一层"音乐场景"的外衣,但剥开之后都是经典的算法内核。这一点其实挺重要的——它不是完全脱离业务的纯算法题,也不是只懂业务就能做出来的题,而是让你把算法能力映射到真实场景中。
2.1 第一题:字符串去重变体,考的是栈和贪心
题目大意:给定一个由小写字母组成的字符串(模拟歌单名集合),要求删除重复字符,使得最终字符串中每个字母只出现一次,且返回的字符串字典序最小。典型的LeetCode 316"去除重复字母"变体。
我在考场上第一眼就认出来了,心里稳了一截。核心思路不是简单去重,而是用贪心加单调栈:遍历字符串时,维护一个栈和每个字符的剩余出现次数;当前字符如果小于栈顶字符,且栈顶字符在后面还会出现,就把栈顶弹出去,这样最终字典序会尽可能小。
def remove_duplicate_letters(s: str) -> str: stack = [] visited = set() remain = {} for ch in s: remain[ch] = remain.get(ch, 0) + 1 for ch in s: remain[ch] -= 1 if ch in visited: continue while stack and ch < stack[-1] and remain[stack[-1]] > 0: visited.remove(stack[-1]) stack.pop() stack.append(ch) visited.add(ch) return ''.join(stack)这道题考察的点很集中:是否理解单调栈的决策逻辑,以及能否处理"visited集合维护"这个细节。写出框架不难,但很容易漏掉remain[stack[-1]] > 0这个条件,一旦漏掉,去重就变成了错误的单调栈。
2.2 第二题:播放列表合并区间,贪心排序没跑了
题目大意:给出一组歌曲在某个节目单中的播放时间段(闭区间),要求合并所有重叠的时间段,输出合并后的区间列表。这是区间合并的经典题,LeetCode 56变体。
思路很简单:先按区间的起点排序,然后遍历所有区间,如果当前区间和上一个合并区间的终点有重叠,就更新终点;否则开启一个新的合并区间。核心点在于排序之后只需要线性扫描,时间复杂度O(n log n),空间复杂度O(n)。
def merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]: if not intervals: return [] intervals.sort(key=lambda x: x[0]) merged = [intervals[0]] for start, end in intervals[1:]: last_start, last_end = merged[-1] if start <= last_end: merged[-1] = (last_start, max(last_end, end)) else: merged.append((start, end)) return merged这类题最大的失分点反而是最简单的部分:边界条件。intervals为空、只有一个区间、区间恰好首尾相接(比如[1, 2]和[2, 3]),都需要仔细处理。合并不是start < last_end,而是start <= last_end,因为闭区间首尾相接也属于重叠。考场上我看到"闭区间"三个字,就知道这里必须用<=。
2.3 第三题:任务依赖调度,拓扑排序的经典场景
这道题明显是压轴题,难度从前两题的LeetCode中等偏下直接跳到偏难。题目场景是模拟一个歌曲制作项目的任务依赖关系:给定n个任务,以及m对依赖关系(a必须在b之前完成),要求输出一个合理的执行顺序,如果任务之间存在循环依赖则返回特定错误。
拓扑排序,用Kahn算法实现。先统计每个任务的入度,然后把入度为0的任务加入队列,依次出队并减少后继任务的入度,每当某个任务的入度降为0就加入队列。最终如果处理过的任务数小于n,说明存在环。
from collections import deque def task_order(n: int, prerequisites: list[tuple[int, int]]) -> list[int]: graph = [[] for _ in range(n)] indegree = [0] * n for a, b in prerequisites: graph[a].append(b) indegree[b] += 1 queue = deque([i for i in range(n) if indegree[i] == 0]) order = [] while queue: node = queue.popleft() order.append(node) for nxt in graph[node]: indegree[nxt] -= 1 if indegree[nxt] == 0: queue.append(nxt) if len(order) < n: return [] return order这道题真正的难点在于"识别出它是拓扑排序"。因为题目包装了一层比较复杂的业务描述,如果对题感不敏锐,很容易被绕进去,试图用DFS加状态标记去硬解。实际上不管用什么方法,核心都是判断有向图是否有环并给出拓扑序列。我当时的做法是读了两遍题,先把依赖关系提取出来画成有向图,然后才动手写代码。
2.4 编程题的时间分配:先把稳的拿下
三道题我用了大概55分钟完成。最后的节奏是:第一题15分钟(含读题),第二题10分钟,第三题30分钟。这个时间分配相当合理——第一题和第二题都属于有把握的题,快速拿下稳住分数,把剩余时间全砸在第三题上。
这里想多说一句:笔试编程题的时间分配比很多人想象的更重要。我见过太多人第一道题反复优化,导致第三题连读题时间都没有。最优策略应该是先花1-2分钟快速浏览全部三道题,形成一个"由易到难"的排序,然后按顺序解。如果某道题卡了5分钟没有思路,果断先跳到下一题。
3. 编程题之外的隐藏考点:代码风格和边界处理
很多人以为笔试只看最终答案对不对,其实不然。面试官拿到你的答题记录时,会看你的代码风格、变量命名、边界处理思路,甚至看你有没有写注释。这些虽然不直接算分,但在技术评审环节会对你的印象分产生不小影响。
3.1 变量命名和结构设计反映工程习惯
我交上去的三道题,变量命名都是清晰可读的类型,比如remaining_count、visited_set、merge_result,而不是a、b、tmp。这一方面是平时写代码的习惯,另一方面也是有意为之——我知道面试官会看答题记录,好的命名意味着好的工程素养。
代码结构上,我尽量把逻辑拆成了几个逻辑块,每个块之间用空行分隔,关键的判断条件旁边会写简短注释。比如在第二题里,我在start <= last_end旁边写了"闭区间重叠",在第三题里写了"拓扑排序完成,检查是否有环"。这不算额外耗时,但对阅读体验的提升是明显的。
3.2 边界情况的验证是拉开差距的关键
很多时候,同样的思路,有人AC有人WA,差的不是算法本身,而是边界情况的处理。以第二题为例,intervals为空的情况、单元素情况、首尾相接情况,每题我都花了几十秒逐一验证。这几十秒在本场笔试里没有浪费——因为一旦某个隐藏边界没处理,整道题一分不得。
第三题的边界更隐蔽:依赖关系里可能包含重复的边,比如(a, b)出现两次,这时候indegree[b]会重复加二,导致入度永远不会降到0,最终误判为存在环。我当时在构图时特意用集合去重了一下,把重复边过滤掉。这个细节题目没明说,但现实中任务依赖清单确实可能出现重复记录,所以考虑到这一点是合理的。
3.3 递归和循环的选择:能迭代就别递归
用递归处理拓扑排序或者数组遍历,写起来很简洁,但在笔试环境里有一个隐患:递归深度过大可能导致栈溢出或超时。我通常优先使用迭代写法,少写递归。第三题用队列实现的Kahn算法就是迭代思路,完全不担心递归层级问题。这也是一种工程判断——面试官会倾向于看到你在写代码时主动规避风险。
4. 考前一周的有效准备:刷题之外还做了什么
腾讯音乐笔试的考察风格整体来说比较"正统"。如果你问我考前一周该怎么准备,我的建议是:不要盲目刷题,先明确考察范围,再有针对性地查缺补漏。
4.1 按出题概率给知识点排优先级
我在笔试前一周把常见的算法知识点按出题概率排了个序,大概是这样的:
| 优先级 | 知识点 | 典型题型 | 准备方式 |
|---|---|---|---|
| 高 | 数组/字符串处理 | 双指针、滑动窗口、单调栈 | LeetCode 中频题刷 2 遍 |
| 高 | 排序与贪心 | 区间合并、任务调度 | 熟练掌握 sort + 扫描模式 |
| 高 | 数据结构基础 | 哈希表、栈、队列、堆 | 理解适用场景和复杂度 |
| 中 | 树 | 二叉树遍历、BFS/DFS | 能手写非递归遍历 |
| 中 | 图论 | 拓扑排序、最短路径 | Kahn算法、BFS/DFS |
| 中 | 动态规划 | 背包、子序列、编辑距离 | 掌握经典状态定义 |
| 低 | 字符串高级算法 | KMP、Trie、AC 自动机 | 了解原理,能默写 KMP |
这个优先级不一定完全覆盖所有公司的出题风格,但腾讯音乐确实完全踩在这个表上:数组和字符串是绝对核心,排序和贪心紧随其后,树和图各出了一道场景题。动态规划这次虽然没有出现,但准备阶段不能跳过。
4.2 手写代码能力:没有IDE自动补全也要顺畅
笔试系统一般自带一个简易IDE,有代码高亮,但别指望有智能补全。我在考前专门练习了不依赖IDE的代码编写,尤其是Python的collections.deque、defaultdict、sorted这些高频模块的用法,保证手写时不会卡壳。
另外一个很有效的练习是限时刷题。我在考前两天用LeetCode的模拟考试功能,每天做一套90分钟的题目,严格按考试节奏来。这种做法不会让你短时间内水平突飞猛进,但会消除考场上的"时间焦虑"——当你习惯了倒计时时,真正上考场反而会觉得从容。
4.3 高频题型的"肌肉记忆"
考前的最后阶段,我不再追求做新题,而是把高频题型重新做一遍:区间合并、字符串去重、三数之和、链表反转、二叉树层序遍历、最长回文子串、爬楼梯。这些题在笔试中出现概率极高,就像篮球运动员的投篮热身一样,它们能让你快速进入状态。
我还特意看了一批LeetCode题目的题解区讨论,不是为了背答案,而是看别人的解法和边界处理方式。同一个题,有人用递归,有人用迭代,有人考虑了空输入,有人没考虑——这种讨论能帮你在考场上想得更全面。
5. 考后复盘:如何用笔试经验校准后续春招节奏
笔试结束不代表事情结束。我习惯在笔试后当天晚上花一小时做复盘,因为这时候记忆还新鲜,哪些题卡过壳、哪些知识点没掌握都一清二楚。这个复盘动作的价值,比多刷五十道题还大。
5.1 建立错题和卡壳记录
我把这次笔试题按"完全不会"、"思路对但代码有bug"、"稳定AC"三档分类。第三档说明这个知识点的掌握已经比较扎实;第二档是最有价值的部分,说明思路没问题但实现细节存在盲区;第一档则是后续复习的重点。
具体来说,我这次的选择题里有一道关于数据库复合索引范围的题,属于"思路对但选项犹豫",说明对最左前缀原则的边界条件还不够敏感。我在复盘记录里专门写了一行:重建复合索引时,范围查询右边字段会失效。这种知识点不需要大块时间重新学,只需要在后续复习中每周过一遍,就能形成条件反射。
5.2 笔试如何影响面试准备方向
通过笔试的题型,基本能看出这家公司对技术栈的重点关注。腾讯音乐这场笔试明显偏重基础数据结构和业务场景的结合,数据库和操作系统的题目也占了相当比例。那我后续的面试准备就会相应调整:数据结构方面重点复习哈希表和树的底层实现,数据库方面重点准备索引优化和事务隔离级别,操作系统方面重点看进程调度和内存管理。
还有一个容易被忽略的点:编程题的代码会被面试官看到,所以复试面试中很可能针对某道题追问细节。比如第三题,面试官可以问"如果依赖关系中存在重复边怎么办"、"如果任务数量特别大能不能用DFS做"、"拓扑排序的结果是否唯一"。因此,笔试考完不等于这道题就结束了,你应该在复盘时把每道题可能的追问方向都想一遍。
5.3 春招节奏:笔试之后做什么
春招整体节奏比较紧,笔试后一般2-5天会出结果,通过后紧接着就会约面试。这段时间不用再海量刷题,而是应该做三件事:第一,把笔试复盘中的薄弱点快速过一遍;第二,准备一段清晰的项目介绍,重点突出技术难点和解决方案;第三,熟悉一些高频的面试追问套路,比如"为什么这样设计"、"有没有考虑过其他方案"。
我自己的经验是,笔试后的等待期其实是心态最容易波动的阶段,因为不确定能不能进面试。但与其干等,不如把这段时间当成冲刺期——无论有没有过,这段时间的复习都不会白费,因为其他公司的面试也会用到这些知识。
最终结果出来,这场笔试我顺利通过,进入了后续的面试环节。复盘完这套流程,我最大的感受是:大厂笔试并不是一道不可逾越的门槛,它考的东西非常实在——基础扎实不扎实,代码写得干不干净,边界情况想得全不全,限时压力下稳不稳。这些能力不是临时抱佛脚能补上来的,需要平时写代码时就养成习惯。如果你正准备下一场笔试,我建议你从今天起就有意识地在刷题时练习"不看自动补全、先想边界条件、写清晰变量名"这三件事。坚持两周,你会明显感觉自己上考场的底气不一样了。