每年春招季,字节跳动研发岗的编程题都会被大家翻出来反复研究。作为跟过大厂笔试、也辅导过不少同学备战春招的人,我对字节跳动研发岗笔试的出题风格还是比较熟悉的。这篇汇总不是让你背答案,而是把历年春招研发岗编程题里最高频的几种题型、典型题目思路、易错点和实战技巧一次性讲透。无论你是准备今年春招的在校生,还是想系统提升算法能力的工程师,这篇内容都能帮你少走弯路。
字节跳动的笔试有一个特点:题目描述通常不长,看上去都不难,但真正动手写的时候会发现边界条件特别多,复杂度卡得也很死。不少同学刷题时觉得“思路我会了”,一到笔试环境就翻车,问题往往不是不会做,而是没有针对这套题型的套路做足够的刻意练习。
1. 字节跳动春招研发岗编程题的整体套路
1.1 笔试到底在考什么
先说一个很多人容易误解的点:字节的研发岗笔试,本质上是筛选“代码能落地”的候选人,而不是在选拔竞赛选手。它不考偏题怪题,绝大部分题目都能在 LeetCode 中等难度附近找到对应原型。但它和 LeetCode 有个很大的区别:笔试系统基本都是 ACM 模式,也就是需要你自己处理输入输出,而不是像力扣那样只需要补全函数。这一点非常关键,因为每年都有不少候选人因为不熟悉输入解析而白白丢分。
另外,笔试的时间限制通常是 120 分钟做 3 到 5 道题,题量不算小。这意味着你不仅要会做,还要做得快。我在实际跟进的案例里发现,真正拉开差距的往往不是最难的压轴题,而是前面 2 到 3 道中等题。很多人把时间耗在最后一道题上,导致前面的题没来得及做,或者做了没时间检查,这是非常可惜的。
字节的题目另一个特点是场景化包装。题目会穿上“视频推荐”、“用户反馈”、“机器人运动”这类业务外衣,剥掉包装之后核心还是那些经典算法模型。所以备考的关键之一就是训练“翻译能力”——把一段业务描述快速翻译成数据结构与算法问题。
1.2 高频考点分布与备战优先级
我整理了近几年春招研发岗笔试里反复出现的题型,按出镜率排序大致是这样的:
| 题型分类 | 典型考察点 | 出镜率 | 备战优先级 |
|---|---|---|---|
| 字符串处理 | 连续字符消除、子串匹配、字典序 | 极高 | 最高 |
| 数组与滑动窗口 | 区间统计、去重、特征提取 | 极高 | 最高 |
| 动态规划 | 状态转移、背包、状压 | 高 | 高 |
| 二分答案 | 最值最小化、可行性判断 | 高 | 高 |
| 链表与二叉树 | 指针操作、递归转迭代、遍历 | 中高 | 高 |
| 图论与搜索 | BFS、DFS、拓扑排序、最短路 | 中 | 中 |
| 贪心 | 排序后取最优、区间覆盖 | 中 | 中 |
从这张表能看出一个规律:字节特别重视“把经典算法用在具体场景中”的能力。字符串、数组、动态规划是绝对的三大核心。
这里面还有一个隐藏重点——复杂度分析。笔试系统会对大样例做超时判定,很多题目你写一个暴力解法,样例可以通过,但数据一大就超时。所以做题的时候,每道题都要先估算一下数据范围,想清楚时间复杂度的上限,再决定用哪种解法。
2. 字符串与数组类题型的实战拆解
2.1 字符串处理:连续字符消除与栈操作
字符串题里有一类非常经典的变体,大意是:给定一个字符串,要求消除连续出现的重复字符,消除后如果拼接又形成新的连续重复,则继续消除,直到无法消除为止。早年有一道“聪明的编辑”相关的题目就是这么考的,只是外层包装不同。
这类题最优解不是反复遍历字符串,而是用栈或者双指针模拟消除过程。用栈的思路特别直观:遍历字符,如果当前字符和栈顶相同,就弹出栈顶,否则压入栈中。这样一次遍历就能完成所有消除,时间复杂度 O(n),空间复杂度 O(n)。
def remove_duplicates(s: str) -> str: stack = [] for ch in s: if stack and stack[-1] == ch: stack.pop() else: stack.append(ch) return ''.join(stack)这个代码很短,但容易踩坑的地方恰恰是“连续两个相同的删除,三个相同怎么办”这种细节。栈模拟天然就处理好了:三个相同字符,前两个抵消后,第三个会和新栈顶比较,而不是留在原地。我在给同学讲解时经常用“连连看”来类比:栈顶就是当前最后一个存活角色,新来的角色如果和它相同就同归于尽,否则新角色留在场上。
笔试中这类题还有一个变体:不是消除全部相邻重复,而是“删除连续出现次数大于等于 3 的字符”,并且要求循环处理。这种就需要用“字符 + 计数”的栈来维护。每个栈元素存两个值,一个是字符本身,一个是当前连续出现的次数。当次数达到阈值时,直接弹出,这样代码也不复杂。
2.2 数组与滑动窗口:特征提取与区间统计
数组类题目里,滑动窗口和哈希表组合的出镜率非常高。有一道典型的“特征提取”题,题目会给你一段时间内每帧的特征列表,要求找出连续帧数最多的相同特征组合。剥掉“帧”这层业务外壳,核心就是:在一组序列中,找连续最长的相同元素子段。
这类题用字典记录每个特征最后一次出现的帧号,以及当前连续长度即可。思路一句话:如果当前特征和上一帧相同,连续长度加一,不同则重置为一。但这里容易出错的是“这里说的是连续,不是累计”。很多人一看到“最长”就条件反射想用动态规划或者排序,实际上这个题就是一个线性的状态维护。
再拿另一类高频率题举例:给定一个数组,要求统计所有长度大于等于某个阈值的连续子数组中不同元素的个数之和,或者滑动窗口内的最大值、最小值。这类题的核心就是滑动窗口保持窗口内数据的有序性或者计数信息。最大值最小值的维护可以借助单调队列,不同元素个数可以借助哈希表计数。笔试里最怕的不是思路难,而是实现时没注意窗口边界的更新时机。
从我的经验来看,数组类题目的代码量通常不大,但边界条件极其密集。我建议做题时先把输入范围圈出来,手动构造三个测试用例:最小规模、正常规模、大规模边界。花两分钟自测,往往能救回一道题。
3. 动态规划与搜索类题型的实战拆解
3.1 动态规划:状态定义是最难的一步
字节春招笔试里的动态规划题,很少直接问“01背包”或“最长上升子序列”,而是会把题目包装成业务场景。有一道“毕业旅行”相关的题,本质上是一个旅行商问题的简化版本:给定 n 个城市之间的距离矩阵,要求从起点出发,每个城市恰好去一次再回到起点,求最小花费。n 的范围通常在 20 以内,看到这个范围你就该反应过来要用状态压缩 DP。
状态定义是这类题的关键。定义 dp[mask][i] 表示“当前已经访问过的城市集合为 mask,最后到达城市 i 的最小花费”。其中 mask 是一个二进制整数,第 j 位为 1 表示第 j 个城市已经访问过。转移时枚举下一个要去的城市 k,更新 dp[mask | (1 << k)][k]。
def min_cost(dist, n): INF = float('inf') size = 1 << n dp = [[INF] * n for _ in range(size)] dp[1][0] = 0 for mask in range(size): for i in range(n): if dp[mask][i] == INF: continue for k in range(n): if mask & (1 << k): continue nmask = mask | (1 << k) dp[nmask][k] = min(dp[nmask][k], dp[mask][i] + dist[i][k]) return min(dp[size - 1][i] + dist[i][0] for i in range(n))这段代码是标准的状压写法,但笔试时很多同学会犯一个低级错误:一开始就把起点固定为城市 0,但最后返回的时候没有加上从终点回到起点的路程。这个问题在样例数据里可能不明显,因为样例选的路径恰好是自洽的,一旦换成大样例就全错。
动态规划题还有一个通用建议:先写暴力递归,再用记忆化搜索改写,最后改成递推。在笔试里,记忆化搜索往往比递推更容易写对,因为你是顺着自然逻辑想的,不需要刻意安排遍历顺序。字节的笔试系统不要求炫技,能 AC 就是王道。
3.2 二分答案与贪心模拟:机器人跳跃的套路
这一类题我要重点讲一讲,因为它考察的不是某个具体算法,而是“把最值问题转化为判定问题”的能力。经典的题目是“机器人跳跃问题”:机器人初始能量为 E,按顺序经过 n 个建筑,每座建筑的高度为 H_i。如果当前能量大于建筑高度,则能量增加差值;否则能量减少差值。要求任意时刻能量不能为负,问初始能量的最小值是多少。
如果直接模拟,初始能量不确定,复杂度难以控制。正确的套路是二分答案。二分初始能量,然后模拟整个跳跃过程,判断中途会不会出现能量小于零的情况。判定的复杂度是 O(n),二分的范围可以定在 0 到最大建筑高度之间,总体复杂度 O(n log C),完全够用。
def can_finish(h, energy): for hh in h: if energy < hh: energy -= hh - energy else: energy += energy - hh if energy < 0: return False return True def min_initial_energy(h): lo, hi = 0, max(h) while lo < hi: mid = (lo + hi) // 2 if can_finish(h, mid): hi = mid else: lo = mid + 1 return lo这个题还有一个细节,就是能量在模拟过程中可能非常大,导致后续计算溢出。实际处理中,一旦能量超过最大建筑高度,后面的过程一定安全,可以直接返回 True,避免无谓的计算。这个优化在笔试里加不加都能过,但在面试手撕代码时能体现你的工程意识。
图论搜索类的题目相对少一些,但一旦出现,基本就是 BFS 求最短路、DFS 排列组合或者拓扑排序。这里我的建议是背熟 BFS 的模板:队列 + 访问标记 + 分层。很多候选人在 BFS 里漏了 visited 数组,导致死循环或者超时,这是最常见的低级错误。
从近两年的题来看,图论题逐渐偏向“状态搜索”,也就是状态不仅是坐标,还包括方向、剩余步数、已收集的物品等维度。应对方式是学会给 visited 数组增加维度,不要只记坐标。
4. 链表、二叉树与环状结构的边界陷阱
4.1 链表题:空指针与快慢指针
链表题在字节笔试里不算最多,但几乎是面试手撕代码的必考项。笔试里如果出现,通常和“环”脱不开关系。比如判断链表是否有环、找到环的入口、环的长度这类问题。
先说判断是否有环,最优解是快慢指针。慢指针每次走一步,快指针每次走两步,如果两指针相遇则说明有环。这里有个不太起眼但特别容易错的地方:快指针的移动必须先判断 next 是否为空。我见过不少同学用 while fast and fast.next 判断,但循环内部又直接 fast = fast.next.next,结果在快指针已经为空时再次访问 next 导致空指针异常。笔试系统对这种异常通常就是直接判运行时错误,不太留情面。
def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow == fast: return True return False找到环入口的经典做法是:快慢指针相遇后,让慢指针回到头节点,两个指针同时每次走一步,再次相遇的位置就是环入口。这个结论用数学可以推出来,但笔试时不要求你证明,记住结论直接用即可。
链表的另一个高频点是反转。反转链表虽然基础,但很多人在迭代写法上翻车,因为需要保存 next 指针再修改当前节点的指向。我建议每个准备春招的人都在纸上手写三次反转链表:迭代、递归、前插法。这题能在一分钟内写对,面试官对你基础能力的判断会好很多。
4.2 二叉树:递归转迭代的落地细节
二叉树题目在笔试中的表现比较“两极分化”:简单的题大家都会,难的题(比如最近公共祖先、树的序列化、二叉树最大路径和)一旦出现,就特别容易拉开差距。
对于这类题,我的第一建议是能用递归就用递归,不要强行迭代。笔试环境时间紧张,递归代码容易写对,系统也不会因为你用了递归就扣分。只有当递归深度可能超过 10 万层,或者面试官明确要求不给栈溢出的时候,才考虑用显式栈改成迭代。
二叉树的前中后序遍历改成迭代是基本功,这里我不展开代码,但提醒一个细节:后序遍历的迭代版本,需要记录上一个访问的节点,否则无法区分“右子树还没访问”和“右子树已经访问过”。这是很多人迭代写错的根源。有一个技巧是往栈里压入“节点 + 状态”的组合,状态用来区分当前是第几次弹出,这个套路的通用性最好。
另外,如果题目给的是二叉搜索树,一定要利用它的中序有序性质。比如求二叉搜索树第 k 小的节点,直接中序遍历到第 k 个即可;如果要做“二叉搜索树转换双向链表”,本质还是中序遍历,只是在中序遍历的过程中调整指针指向。这类题代码量不大,但对遍历过程的理解要求很高。
我还会再强调一次:笔试里树的输入往往不是链表式的节点,而是直接给一个数组,你需要按照层序关系自己重建树。这部分如果要写,建议自己实现一个 build_tree 函数,提前准备好比现场调试要靠谱得多。
5. 编程题实战流程与自我提效方法
5.1 模拟笔试环境的完整自测流程
准备字节春招笔试,最忌讳的就是只在 LeetCode 上刷题,从来不模拟真实笔试。LeetCode 是函数模式,输入输出系统都帮你处理好了,但字节的笔试系统是 ACM 模式,你需要自己写数据解析。我在给同学做模拟面试时,发现至少有三分之一的人卡在输入解析上,而不是卡在算法本身。
我建议你至少在正式笔试前做三次完整的模拟:找一套往年真题,设定 120 分钟倒计时,使用牛客网或者赛码网这类平台,严格按照真实流程走一遍。模拟的时候注意三件事:第一,输入用 sys.stdin 读取,不要用 input() 一行一行读,速度差异在大数据量下还是很明显的;第二,输出要严格匹配题目的格式要求,多一个空格少一个空格都算错;第三,就算题目简单,也要坚持写完自测用例再交卷。
import sys def main(): data = sys.stdin.read().strip().split() if not data: return n = int(data[0]) nums = list(map(int, data[1:1 + n])) # 核心逻辑 print(result) if __name__ == "__main__": main()5.2 调试与自查的细节经验
笔试过程中,调试时间是非常宝贵的。我发现高频错误集中在几类:数组越界、未考虑空输入、整数溢出、输出格式不对。与其等系统报错,不如在写代码时就把这些预防掉。
一个很实用的习惯是“三用例法”。每道题写完,手动构造三个用例:一个最小输入(比如 n=1 或者空数组)、一个正常输入、一个极端输入(比如全是相同元素、逆序、超大数据)。运行一遍,确保三个都能通过。这个习惯大概会花掉你两三分钟,但能救回很多低级错误。
另外,如果你用的是 Python,注意大整数不会溢出,但如果你用 C++ 或 Java,就要格外注意 int 的范围。字节的题目虽然不常故意卡溢出,但某些动态规划题累加之后很容易超过 int 范围,建议直接声明成 long 类型,省得后面返工。
还有一个很多人忽略的点:笔试平台的代码编辑器和本地 IDE 体验差异很大,没有代码补全和高亮,缩进要靠手动。建议在正式笔试前,至少用平台内置的编辑器写几道题,提前适应这种“原始”的编码环境,避免现场因为编辑器用得不顺手而影响状态。
6. 常见问题与排查技巧实录
6.1 高频易错点速查表
这是我在辅导过程中总结的最常见错误,按出现频率排序:
| 错误类型 | 典型场景 | 解决思路 |
|---|---|---|
| 输入解析错误 | 读入多行数据时错用 input() | 全部读入后 split,再按需取用 |
| 空指针 | 链表操作时 fast.next 为 None | 循环条件加上 fast and fast.next 判断 |
| 边界越界 | 数组下标 i+1 访问到末尾之外 | 循环前先确认长度条件 |
| 状态遗漏 | BFS 忘记标记已访问 | 入队时立刻标记 visited |
| 复杂度超时 | 暴力解法在大样例上跑不完 | 提前估算数据范围,改用二分、滑动窗口或 DP |
| 输出格式错误 | 多输出了空格或换行 | 用 join 统一拼接输出 |
这里面最值得提醒的是第 6 条。很多题目的输出要求是“每行一个结果”或者“用空格分隔”,有人图省事用 print 在一行里多次调用,结果输出中间带括号和逗号,直接判错。建议所有输出都用字符串拼接后一次性打印,这样最可控。
6.2 我从实际陪跑中总结的几条心得
最后聊几句个人经验。字节春招研发笔试的难度,整体是逐年稳定的,没有出现忽难忽easy的情况。如果你刷完了 LeetCode 前 200 道高频题,再加上 3 次以上模拟笔试,大概率能通过笔试这一关。真正决定你能不能拿到面试机会的,往往是你能不能稳定发挥,而不是某一道压轴题有没有做出来。
我的实际建议是:准备阶段按“题型、套路、模板”三个维度整理笔记,比如二分答案的判定模板、状压 DP 的状态转移模板、BFS 的 visited 模板。笔试当天的策略则是“先易后难、及时止损”,每道题给自己设一个 25 分钟的硬上限,超过就跳。宁可保证 3 道题高质量通过,也不要 5 道题每题都半成品。
另外,春招笔试通常是可以多次投递、多个部门分开安排的。如果你第一次笔试发挥失常,不要气馁,及时复盘是哪类题失分,下一次针对性补强即可。我见过太多同学因为一次失利就心态崩掉,反而浪费了后面的机会。
编程题的准备没有捷径,但确实有方法。把高频题型吃透,把几个核心模板练成肌肉记忆,再把输入输出的细节小坑处理好,你就能在字节春招研发岗的笔试中拿到一个稳定的发挥。