
收到腾讯音乐娱乐TME2023暑期实习生招聘技术类笔试I的邀请邮件是在一个工作日的下午。我当时正在图书馆里刷LeetCode看到邮箱提醒弹出来第一反应是确认考试时间第二反应是有点懵邮件里没有给具体的题目范围只写了时长、若干选择题和四道编程题。我投递的是后端开发岗在此之前也参加过其他几家的笔试但TME这套题给我的整体感觉是不偏不怪但很考基础尤其考你在有限时间内能不能把思路转化成可运行的代码。这篇复盘我拖了半年才写主要是想把笔试中暴露出来的问题记下来。如果你正在准备TME或者同类大厂的技术类暑期实习这篇内容可以帮你快速了解笔试大概长什么样、哪些坑值得提前避开以及考完到面试之间该怎么衔接。要提前说明的是每批次题目大概率不一样我更想讲的是思路和方法而不是让你背题。笔试本身只是整个流程的一环真正决定能不能进面试的往往是你能不能在有限时间里稳定输出。1. 从投递到开考这次笔试的时间线与准备清单1.1 投递渠道、岗位选择与时间线我是在五月底通过腾讯音乐娱乐招聘官网投递的后端开发暑期实习岗位没有走内推。投完之后大概两周没有动静中间一度觉得自己简历初筛没过。7月初收到笔试I的通知时距离投递已经过了一个多月所以如果你投完简历后暂时没消息不必太早放弃大厂流程就是会有一段静默期。身边一起准备的同学有走内推的也有在第三方招聘平台投递的普遍反馈是投递渠道会影响收通知的速度但不会影响笔试内容。内推同学的状态更新更及时仅此而已。TME的技术类笔试面向的岗位很宽后端、前端、客户端、测试开发、算法等都可能在同一套卷子里部分方向可能有附加题或者选做题。我是投后端的以下内容基本以后端视角为主但选择题部分对所有技术方向应该是通用的。1.2 考前一周我做的三件事由于不知道笔试具体考什么我把考前一周押在了最稳的三件事上。第一集中刷高频题型。我在LeetCode上按“数组、字符串、链表、二叉树、动态规划、二分查找”这几个标签刷题每天四到五道不求难题偏题只求常见套路熟练。事后证明这个方向是对的TME的编程题没有出怪题几乎都在这些经典题型的变形范围内。第二过了一遍计算机基础高频题。这部分是为了应对选择题里占比不小的操作系统、网络和数据库考点。我没有重新啃书而是直接看面经里经常出现的知识点进程线程区别、死锁条件、TCP握手挥手、索引失效场景、事务隔离级别等。目标不是背答案而是做到看到一个选项能快速判断对错。第三抽了一个晚上熟悉在线笔试平台的输入输出。这一点很多人都忽略了。本地跑代码和在线判题完全是两回事尤其是多行输入、多组测试用例的情况不提前练一下考场上很容易在IO上浪费二十分钟。1.3 笔试邮件里最容易忽略的两条信息邮件里最重要的信息往往不在正文标粗的“考试时间”里而在后面的注意事项。我这场明确写了需要开启摄像头监控并且浏览器不能切出考试页面太多次。具体次数标准没有公开但根据以往经验频繁切换窗口很可能被判违纪或者至少会收到后台警告。所以我才在考前把常用模板提前复制到在线编辑器的草稿区避免考试时反复拷贝粘贴。另一条容易被忽略的是考试时间跨度和可进入时间。有的笔试允许你在某个时间段内任意时间开始但计时从你点击“开始答题”起算。这就意味着你可以晚点进但进去之后时间就固定了。我习惯提前十五到二十分钟登录把身份证件放在旁边网络、电源、耳机全部检查一遍再点开始避免因为设备问题浪费宝贵的答题时间。2. 笔试题型全景题量、分值与我实际的时间分配2.1 我这场笔试的题型构成进入考试页面之后系统会先展示一个总体说明。我这场的情况大致是总时长120分钟前面是一批选择题后面是四道编程题。选择题涵盖单选和多选混合在一起数量大概在30到40道之间。编程题分值占比更高总体来看笔试过线的关键在于编程题而选择题决定你能不能稳过。选择题覆盖的知识面很广我粗略统计了一下数据结构、操作系统、计算机网络、数据库、编程语言基础均有涉及偶尔还有一两道Linux命令和编译原理相关题。深度不算大基本都是概念辨析和简单推导比如给一段代码问输出结果、给一个场景问该用什么数据结构、给一个数据库索引问是否会命中。这些题单看难度都不高但放在一起考的是知识宽度。编程题四道由易到难排列难度曲线非常明显。第一道是字符串处理第二道是动态规划第三道是二叉树相关第四道需要二分解法。前两道属于“只要思路对了很快能写出来”的题第三道考基础扎实程度第四道需要多想一步。2.2 时间分配方案先扫题再定顺序我的时间分配策略是打开试卷后先不急着做任何题用三到五分钟把四道编程题全部看一遍。这件事很重要它可以让你在还没进入做题状态前就掌握全局知道哪道题是送分题、哪道题需要留足时间。看完之后我决定先做选择题再做编程题因为选择题卡壳的概率更高把它放在精力最充沛的前半段是最好的。选择题我给自己定的预算是40分钟平均每道题一分钟左右。遇到一眼看不出答案的题先用排除法筛掉明显错的选项然后立刻标记不做过多纠缠。40分钟一到不管还剩几道直接切到编程题。编程题四道总共留了约75分钟剩余时间机动分配。最后留五分钟左右检查提交状态确保每道题都提交成功而不是只“保存了代码”。这里特别想提醒一点不要把选择题全部做完再做编程题。选择题里很容易出现一道你刚好“有点印象但记不牢”的题你会忍不住花五分钟去回忆但五分钟在编程题里可能就够多调试一个测试用例了。我在考场上给自己定的硬规则是一道选择题超过两分钟没做出来立刻跳过绝不死磕。2.3 如果遇到不会的题止损与得分最大化笔试最容易出现的心理崩盘是卡在第二道题上后面明明会做的题也没时间做了。我的做法是先把所有编程题看一遍标记出“一定会写”和“有机会写”的题从最有把握的开始做。如果某道题思路卡住超过15分钟果断停笔换下一道等做完其他所有能拿的分再回头想这道题。编程题通常按通过率给分不是非黑即白。所以就算你只想到了暴力解法也要写上去。很多时候暴力解能过一半甚至更多的测试用例这个分数比空着强太多了。我印象中第四道题一开始只写了二分框架边界条件还没调完但我先把一个O(n*m)的暴力版本提交了至少保证有基础分然后再继续优化。这种“先保底再优化”的思路在时间紧的笔试里比追求完美解法重要得多。3. 四道编程题复盘思路、代码和交卷前的检查点3.1 字符串题删除一个字符后判断两串相等第一道编程题让我印象挺深因为它虽然简单但暗含一个很容易写错的细节。题目大意是给定两个字符串判断能否通过删除其中一个字符串里的一个字符让两个字符串相等。如果两个字符串本身已经相等也算通过。我的第一反应是双指针两个指针分别指向两个字符串开头从左往右逐个字符比较。遇到不同字符时理论上可以有两种操作要么删掉第一个串里还没匹配的字符要么删掉第二个串里还没匹配的字符。问题在于如果两个字符串长度差大于1那不管怎么删都肯定不相等可以先返回false。如果长度差刚好是1或者0就需要在遇到第一次不匹配时尝试往前跳一次后面必须完全匹配。这类题最容易踩的坑是只考虑“跳过长串的一个字符”没有考虑两个串长度相等时其实不能靠删除一个字符来让它们相等。因为删除一个字符会让长度差变成1所以如果原串长度相等唯一能通过的情况只有两个串已经完全相同。我不小心把这种情况也当成“删一个字符”来处理了好在本地测试用例提前暴露了这个问题。def can_be_equal_by_one_deletion(s: str, t: str) - bool: if s t: return True if abs(len(s) - len(t)) ! 1: return False i, j 0, 0 deleted False while i len(s) and j len(t): if s[i] t[j]: i 1 j 1 else: if deleted: return False deleted True if len(s) len(t): i 1 else: j 1 return True这道题的时间复杂度是O(n)空间复杂度是O(1)。交卷前我特意检查了那几个边界两个串完全相同、第一个串比第二个串长、第二个串比第一个串长、差异出现在最后一个字符。批量过一遍之后我才提交。3.2 动态规划题环形数组的最大不相邻子序列和第二道编程题是“打家劫舍”的变体但加了一个环形限制。题目大意是一个环形排列的数组相邻两个元素不能同时选问能选出的最大和是多少。经典线性版本大家应该都很熟使用两个状态变量滚动更新就行。环形版本需要额外考虑“首尾不能同时选”的限制。当时的思考过程是环形问题最常见的处理方式就是分类讨论分两种情况分别调用线性版本。第一种情况不选第一个元素第二种情况不选最后一个元素取两种情况的最大值。因为我投的是后端岗所以选择用Python写思路会更直白。def rob_linear(nums: list[int]) - int: not_rob, rob 0, 0 for num in nums: new_not_rob max(not_rob, rob) new_rob not_rob num not_rob, rob new_not_rob, new_rob return max(not_rob, rob) def rob_ring(nums: list[int]) - int: if len(nums) 1: return nums[0] if len(nums) 2: return max(nums[0], nums[1]) return max(rob_linear(nums[1:]), rob_linear(nums[:-1]))这种方法的时间复杂度是O(n)空间复杂度O(1)。我写完后特意试了长度为1和长度为2的数组因为环形拆分的边界条件最容易在这两个最小长度上出错。另外还要注意如果数组元素全部都是非负数这道题会比较友好但如果出现负数那么“不选”状态的结果要保证不会被负数拖低滚动更新里的max就起到了这个作用。考场上我一开始犯了个错把rob_linear(nums[1:])写成了rob_linear(nums)忽视了环形首尾相邻的限制。虽然示例用例不明显但自己补了一个[2, 3, 2]的用例立刻暴露了问题正确答案是3而不是4。这种细节靠检查代码逻辑很难发现最有效的方式就是多跑几个自己构造的边界例子。3.3 二叉树题校验二叉搜索树第三道编程题是给定一棵二叉树判断它是否是一棵合法的二叉搜索树。所谓合法是指对于每个节点左子树的所有节点值都严格小于它右子树的所有节点值都严格大于它。我当时很快就想到了中序遍历因为二叉搜索树的中序遍历结果一定是严格递增序列。只要在中序遍历过程中维护一个“上一个访问到的值”如果当前值不大于上一个值就判定不合法。我选择用迭代版中序遍历实现因为笔试环境里递归可能遇到栈深度问题而且迭代版也不难写。def is_valid_bst(root) - bool: stack [] prev None while stack or root: while root: stack.append(root) root root.left root stack.pop() if prev is not None and prev.val root.val: return False prev root root root.right return True这里特别要注意一个约定题目里如果强调“严格小于/严格大于”那么相等也必须返回false。我印象里TME这道题题干明确说了“strictly less than”所以我在prev.val root.val里用了大于等于。如果你的题目没有明说建议在答题前先写注释确认或者用测试用例试探。这道题leetcode上的变体版本为简化使用但如果要求不严格递增就得改成。这种题在笔试里出现频率很高因为它同时考了二叉树遍历、递归/栈的使用以及对二叉搜索树定义的准确理解。我的检查点是空树是否返回true、单节点树是否返回true、是否考虑了重复值、是否考虑了大整数边界。全部过一遍之后我提交了。3.4 二分答案题分割数组的最小化最大值第四道编程题难度明显上来了题目大意是给定一个非负整数数组和一个整数m要求将数组分成连续的m段问这m段各自的和中最大值最小可以是多少。这是一个非常经典的“二分答案”题但第一次见的话思路不一定能立刻转过来。我一开始也想过动态规划但一看数据范围比较大动态规划的O(n^2m)复杂度很可能过不了。于是立刻切换思路能不能二分“每段和的最大值limit”然后判断在这个限制下是否能将数组分成不超过m段。判断的逻辑很简单贪心地从左到右扫描当前段累加和超过limit就新起一段最后统计段数是否小于等于m。def can_split(nums: list[int], m: int, limit: int) - bool: segments 1 cur_sum 0 for num in nums: if cur_sum num limit: segments 1 cur_sum num if segments m: return False else: cur_sum num return True def split_array(nums: list[int], m: int) - int: left max(nums) right sum(nums) while left right: mid (left right) // 2 if can_split(nums, m, mid): right mid else: left mid 1 return left二分下界是数组中的最大值因为每个元素至少要单独成一段上界是整个数组的和。整个算法的时间复杂度是O(n * log(sum))空间复杂度O(1)。这道题我检查时的重点有几个一个是can_split的初始段数应该设为1而不是0另一个是单个元素大于limit的情况。如果数组中出现某个元素本身大于limit贪心算法里的cur_sum num limit会直接把段数累加但更稳妥的方式是在贪心前先判断max(nums) limit直接返回false不过我这里利用左边界是max(nums)所以二分过程中limit不会小于最大值。交卷前我还注意了一个点mid的向下取整在left right的循环里不会造成死循环因为当can_split为true时right mid缩小范围为false时left mid 1也缩小范围。检查完边界这道题才算是稳了。4. 选择题覆盖的六个知识域以及我记忆最深的易错点4.1 数据结构与算法选择题考数据结构的部分比较基础重点集中在数组、链表、栈、队列、哈希表、堆和图。让我印象最深的一道题是问数组和链表的区别看起来太简单但选项里有一条“数组可以在O(1)时间内删除任意一个元素”这个表述是错的因为数组删除元素需要移动后续元素平均是O(n)。这种题就是考察你是否真的理解而不是背过“数组适合随机访问链表适合插入删除”的口诀。堆相关也考了一道向一个大小为n的二叉堆中插入一个元素平均时间复杂度和最坏时间复杂度分别是多少。答案分别是O(1)和O(log n)。很多人容易忽略平均情况因为堆的插入通常先放到最后然后上浮很多情况下上浮次数很少。哈希表考了冲突处理方法问链地址法和开放地址法的区别。这类题不难但需要记得线性探测的“聚集”现象以及链地址法在负载因子过高时会退化为链表。4.2 操作系统操作系统这块选择题占比不低。考到进程和线程的区别这是高频题。有一道选项说“同一进程内的多个线程共享独立的栈”这明显是错的进程内的线程共享堆和代码段但每个线程有自己的栈。死锁的四个必要条件也考了迷惑选项是把“资源剥夺”写成必要条件。实际上剥夺是多用于死锁预防的一种方法死锁必要条件包括互斥、持有并等待、不可剥夺、循环等待四个缺一不可。页面置换算法也出现了一道问LRU算法在缓存大小为3、访问序列为某串时缺页次数是多少。这类题只要耐下心模拟一遍就不会错但考试时容易因为紧张算错。我的经验是在草稿纸上画一个三格窗口每访问一个页面更新一次同时记录缺页次数不要心算。4.3 计算机网络网络的选择题集中在TCP和HTTP。TCP的三次握手和四次挥手是必考点题目会问第三次握手失败时两端分别处于什么状态或者四次挥手中TIME_WAIT状态出现在主动关闭连接的那一方。这类题不太难但需要记得TIME_WAIT的持续时间是2MSL。HTTP状态码也考了一两个比如301和302的区别301是永久重定向302是临时重定向。还有一个选项比较坑“404表示服务器内部错误”这显然不对500才是内部错误。网络这块整体感觉偏向基础概念不涉及太深的协议源码。4.4 数据库数据库考了事务隔离级别和索引失效场景。脏读、不可重复读、幻读三者对应的隔离级别属于经典中的经典。有一道题给出了一个场景同一个事务内两次查询同一范围的数据得到的结果集不同问这是哪种问题。答案是不可重复读还是幻读要看题干描述的是“某一条数据值变了”还是“多出了几行”。我做的时候差点选错还好题干里写的是“结果集行数发生变化”那就应该选幻读。索引失效考了一道非常常见的在索引列上进行函数运算比如WHERE YEAR(create_time) 2023这个情况下索引会失效。另一个选项是对索引列进行了隐式类型转换也会失效。这类题没什么技巧只能把常见失效场景记住。4.5 编程语言特性语言相关题目后端方向主要考C和Java类的题。我印象比较深的一道是关于C的static关键字选项覆盖了静态局部变量、静态全局变量、静态成员函数和静态成员变量的特性。其中“静态成员函数可以直接访问静态成员变量也可以直接访问普通成员变量”是错的静态成员函数没有this指针不能直接访问非静态成员。Java则考了volatile和synchronized的区别选项里说“volatile可以保证原子性”是错的。volatile只能保证可见性和有序性不能保证复合操作的原子性。还有一道问Java垃圾回收的finalize机制这一块我平时接触不多属于靠排除法猜的。4.6 其他零星考点Linux、编译原理与计算机组成选择题里还零星出现了Linux命令和编译原理的题。Linux考了chmod的数字权限比如chmod 755表示文件所有者可读可写可执行用户组和其他用户只读只执行。还有一个选项是关于kill -9和kill -15前者是强制终止系统不会给进程清理资源的机会后者是通知进程自行退出。这类题属于知道就会不知道就只能蒙。编译原理考了一道针对“中间代码生成”的描述问三地址码的作用。我印象比较深是因为这道题让我意识到基础知识复习不能只盯三大件编译原理和计组虽然频次不高但确实会出现在试卷里。时间充足的话至少把词法分析、语法分析、中间代码、目标代码生成这些概念过一遍。5. 在线笔试里的非技术坑平台、输入输出和切屏5.1 平台环境与本地IDE的搭配技术类笔试一般有两种环境一种是纯在线编辑器不让你用本地IDE另一种是允许使用本地编译器但是由在线监控插件盯着。我这场是允许开本地IDE的看起来很方便但它同时监控切屏次数。也就是说如果你频繁在浏览器和本地IDE之间切换后台会记录风险操作。我的做法是把本地IDE当成“草稿纸”主要在里面调试逻辑调试通过后再手动粘贴到在线编辑器。为了减少切屏次数我会在考试开始前把输入输出模板、常用库的import语句都提前写在在线编辑器里这样切一次就能粘贴完整代码不用来回切。如果你参加的那场不允许本地IDE那就要提前习惯在线编辑器的补全能力。很多在线编辑器的自动补全很弱没有代码格式化也没有错误提示写C时容易漏掉分号。提前用平台模拟题练一次能明显降低这种场外因素影响。5.2 输入输出格式带来的真实翻车现场笔试翻车最高发的地方就是本地跑通了提交却一直报错。我第一道编程题就差点栽在这里本地我用的是sys.stdin.read()一次性读取但在线判题需要按行读取多组测试用例如果用例数量不固定一次性读取的写法可能把多个用例混在一起。我常见的输入模式有这几种第一行一个整数表示测试组数T接下来T组数据或者整个文件只有一组数据又或者多组数据直到文件结束。你必须在做题前先搞清楚输入模式否则代码处理的第一行就可能出错。一个比较稳妥的做法是用sys.stdin.read().split()拿到所有token之后再根据题意相邻消费这样能兼容大多数输入场景。输出格式也一样。题目如果要求“每个结果占一行”你就老老实实拼接到一个list里最后统一print不要在循环里print带额外空格。有些题甚至要求每组之间有一个空行容易被忽略。交卷前务必重新读一遍题目最后的输出说明。5.3 做题顺序与最后检查的微操在线笔试的时间管理其实在交卷前五分钟最能看出差距。我的习惯是留出最后五分钟逐一点开四道编程题确认每一道都处于“已提交”状态而不是“草稿”状态。听起来很傻但确实有人因为只保存了代码没点提交整道题零分。还有一个小技巧把每道题里自己写的关键测试用例注释在代码块开头方便检查时快速回忆思路。比如我写了几个边界用例在注释里如果交卷前还有时间就重新跑一遍看是否仍是预期输出。这样既不会增加提交内容也能让自己安心。选择填空部分我会把拿不准的题号记在草稿纸上最后有时间就回来看。但这里有个限制如果切屏次数有风险尽量不要反复跳转回选择题。所以我的策略是选择题做完一遍之后除非有明显思路了否则绝不回头看。6. 笔试结束后的三天自评、补漏与面试衔接6.1 交卷后的第一件事趁热复盘交卷后的一两个小时记忆还热乎着是复盘的最好时机。我不会立刻对答案而是先把四道编程题的思路和代码重新写进本地笔记尽量还原自己在考场上的版本。哪怕题目记不全至少把核心解法、卡住的点、优化方向记录下来。这份记录在后面的面试阶段非常有用因为面试官如果看到你的简历可能会问你笔试里印象最深的题是怎么做的。还有一个容易被忽略的动作记录每道题花了多长时间。我事后统计了一下第一道字符串题花了12分钟第二道动态规划花了18分钟第三道二叉树花了20分钟第四道二分答案花了35分钟合计85分钟基本和计划吻合。有了这个数据后续再笔试或者面试手撕代码时就能更准确地判断自己的速度是否合格。6.2 查漏补缺选择题错题对应的知识块笔试结束后我还把印象中不太确定的选择题整理成了清单。按照知识域分类之后发现我的主要漏洞集中在数据库的隔离级别、操作系统LRU模拟题、以及编译原理的概念题。这些东西单个来看都是小点但放在笔试里就是一道实实在在的扣分题。我当时用了两天时间把数据库事务隔离级别彻底搞清楚了画了一个表对比各种隔离级别下的脏读、不可重复读、幻读是否存在顺手把LRU的模拟过程在纸上跑了十几遍。这种查漏补缺不只为这场笔试更是在为接下来的面试打基础。果然后来的几场面试里这些问题反复出现尤其是数据库隔离级别几乎必考。6.3 从笔试到面试更重要的几件事笔试过后一般两三天内会收到结果。等待期间我只做两件事一是复习项目把简历上写过的每个项目从背景、难点、技术选型到最终效果全部用STAR法则梳理一遍准备给面试官讲15分钟版本和3分钟版本二是保持每天两三道算法题的手感重点练二叉树、动态规划、数组类题目因为这几类是面试手撕代码的重灾区。说实话笔试通过与否并不能完全代表你的真实水平但它决定了你能不能拿到面试的入场券。我自己的感受是TME这套笔试题不会故意刁难人它考的是基础、稳定和应变能力。如果你能把常见题型的套路练熟把基础概念理解透再把IO和平台细节处理好通过的希望是很大的。有个细节我一直觉得很有用笔试结束后不管结果如何我都没有删掉那晚的本地调试代码。后来面试复盘时我甚至把其中一道二分答案题又优化了一版举一反三写成了一个小工具。这种“一次笔试持续沉淀”的习惯可能是整个求职季给我最大的帮助。