每年春节一过,找我咨询春招的人就多起来。问得最多的就是:"字节跳动研发岗的编程题,到底考什么?" 我做过几年后端研发,也断断续续参与过校招面试流程,平时会收集公开面经和身边同学的复盘,把这个问题整理过不少次。今天这篇就把我整理出来的东西一次性摊开:题型分类、每类题的解题套路、隐藏评分点、30天备战时间线,以及现场做题的节奏。如果你正在准备大厂研发岗春招,不管投的是后端、客户端还是测试开发方向,这篇文章里对应部分都能直接拿去用。
先说明一点:这不是官方题库,也不存在所谓的官方汇总。字节跳动的春招笔试和面试编程题是从一个很大的题库里抽的,不同方向、不同批次会有差异。但万变不离其宗,题型高度集中在几个方向,掌握了这些方向,怎么抽你都不慌。
1. 一个过来人对字节跳动春招研发编程题的还原
1.1 我在笔试和面试里遇到的编程题形态
我经历过两种完全不同的编程题场景。第一种是笔试机考,3道题,90分钟,系统自动判题。这种考试通常偏算法,难度呈梯度:第一道偏简单,第二道是主力中等题,第三道开始上难度。第二种是面试环节里的现场手写代码,面试官会直接给你一个在线编辑器,或者让你共享屏幕在本地IDE里写,重点考察你的思考过程、边界处理和代码习惯。
很多人会忽略一个细节:笔试和面试的判题标准完全不一样。笔试只看最终跑分,你代码写得再烂,只要AC就能过;面试恰恰相反,哪怕你最终没写出完美代码,只要思路清晰、能和白板面试官顺畅讨论,也能拿到大部分过程分。所以备战策略要分开:笔试练速度和正确率,面试练表达和结构化思考。
字节跳动的研发岗笔试,有些场次要求你在ACM模式下编程,也就是自己处理输入输出,从input()读入,用print()输出。而有些场次是核心代码模式,只需要把核心函数补全。我建议所有准备春招的人默认按ACM模式准备,因为会这个再遇到核心代码模式是降维打击,反过来就有点慌。
1.2 从公开信息看近两年题型分布
这些年我把能看到的面经、讨论帖、学员反馈都过了一遍,发现考题类型分布其实相当稳定。
| 题型类别 | 出现频率 | 常见特征 |
|---|---|---|
| 字符串与哈希 | 很高 | 变位词、频率统计、子串判断、字符重排 |
| 数组与双指针 | 很高 | 排序数组、原地修改、滑动窗口、前缀和 |
| 二叉树与DFS/BFS | 高 | 树的遍历、路径问题、最近公共祖先 |
| 动态规划 | 中高 | 一维/二维DP、编辑距离、背包、状态压缩 |
| 二分与贪心 | 中 | 有序结构、答案可行域判断、最大化最小值 |
| 并发与工程实现 | 中低 | 多线程交替打印、缓存设计、LRU/TTL |
这个分布不是巧合。字节跳动这类大厂筛人的底层逻辑是:你要有扎实的数据结构基础,能对时间空间复杂度保持敏感,并且能在高压下把思路转成可运行的代码。至于"IPD研发流程""CDCP概念"这类研发管理热词,更多是设计题或项目提问环节的背景知识,不会直接出现在编程题里。但如果你投的是Java研发,偶尔会遇到以"企业研发成果管理系统"为背景的工程实现题,核心还是考缓存、并发、数据组织这些基本功。
2. 高频题型拆解:每一类题到底在考什么
这一节是重点。我不方便把真实原题照搬出来,但可以用每个方向最具代表性的模拟题,把解题思路讲透。你只要把这几类题的通用解法吃透,上考场遇到变体就不会抓瞎。
2.1 字符串与哈希:送分还是陷阱
字符串题看起来简单,翻车率却很高。最经典的问法是"给一个字符串数组,返回出现次数最多的K个单词,次数相同按字典序升序"。很多人第一反应是排序,直接O(n log n)一把梭,这样写没错,但如果K远远小于n,最优解是用堆维护TopK。
import collections import heapq def top_k_words(words, k): cnt = collections.Counter(words) heap = [] for word, num in cnt.items(): heapq.heappush(heap, (-num, word)) if len(heap) > k: heapq.heappop(heap) res = [] while heap: res.append(heapq.heappop(heap)[1]) return res[::-1]这里有个关键点:堆的排序规则。Python的堆默认按元组第一个元素排序,我们把次数取负数,这样次数大的会排到堆顶。次数相同再按字典序升序,所以元组里第二项直接放word。别小看这个细节,面试官很喜欢在排序比较规则上设坑。
这类题真正考的是两点:一是是否熟悉哈希表的底层结构,二是能不能在排序规则上保持清醒。做题前先问自己:数据规模多大?K接近n还是远小于n?如果有多个相同次数字典序怎么排?把这些想清楚再动手。
2.2 双指针、滑动窗口与单调栈:数组题三板斧
数组类题目在笔试里占比很高,其中滑动窗口和双指针是出镜率最高的。
模拟题:"给定一个字符串,找出其中不含有重复字符的最长子串长度。" 这是很典型的滑动窗口题,核心模板我建议你背下来:
def length_of_longest_substring(s): window = set() left = 0 ans = 0 for right, ch in enumerate(s): while ch in window: window.remove(s[left]) left += 1 window.add(ch) ans = max(ans, right - left + 1) return ans这个模板的价值在于,它把窗口维护逻辑抽象成了"右指针不断扩张,左指针条件收缩"的固定套路。遇到"最长不重复子串""最小覆盖子串""字符串排列"这类问题,直接套模板能省去大量推导时间。
另一个高频的是前缀和。比如"给定数组,求有几个连续子数组的和等于k",暴力是O(n^2),但配合哈希表记录前缀和出现次数就能压缩到O(n)。这类思想我在字节的笔试和面试题里见过很多次,本质上都是找连续区间上的重复计算,然后用空间换时间。
2.3 二叉树与DFS/BFS:递归思维的试金石
二叉树是面试官最喜欢用来考察递归理解的载体,因为代码很短,但递归的每一步都要想清楚。
模拟题:"给定一棵二叉树,找到两个指定节点的最近公共祖先。" 递归解法非常经典:
def lowest_common_ancestor(root, p, q): if not root or root == p or root == q: return root left = lowest_common_ancestor(root.left, p, q) right = lowest_common_ancestor(root.right, p, q) if left and right: return root return left if left else right很多初学者看到这个代码会懵:为什么返回非空的那一侧?其实逻辑是,如果p和q一个在左子树一个在右子树,当前节点就是答案;如果都在某一侧,那答案在该侧内部,当前节点往上返回时只把该侧的结果继续传上去。
DFS和BFS的选择也很关键。求最短路径、层序遍历、逐层扩散的问题优先BFS;求所有路径、判断连通性、回溯类问题优先DFS。字节的编程题里,树和图的题经常不是直接裸考,而是包装成一个场景题,比如"网络节点间的最少跳数"本质上就是BFS求无权图最短路。做题时要能扒掉外壳看内核。
2.4 动态规划:状态定义决定成败
动态规划是很多人的老大难,字节的编程题里DP题目占比不低,但很少考特别偏的模型,集中在基础模型上。
模拟题:"计算两个字符串的编辑距离,即通过插入、删除、替换字符,最少操作几次能互相转换。" 这是经典的二维DP。状态定义:
dp[i][j]表示word1前i个字符转换成word2前j个字符需要的最少操作数。
转移时如果word1[i-1] == word2[j-1],那么dp[i][j] = dp[i-1][j-1];否则取三种操作的最小值再加1:
def min_distance(word1, word2): m, n = len(word1), len(word2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 return dp[m][n]DP题最怕的不是不会写转移方程,而是状态定义错了。我总结了一个笨办法:先想清楚"我需要记录哪些信息才能做决策";然后用一维、二维甚至三维把这些信息表达出来;最后再写转移。面试时如果一时没思路,可以先把棋盘或递归树画出来,找重复子问题,这比空想要快得多。
2.5 二分与贪心:边界条件是分水岭
二分查找的代码不难,但边界条件经常让人一头包。笔试场上最稳的是统一使用"左闭右开"或者"闭区间"模板,不要每个题现场发明一种写法。
模拟题:"给定一个数组,代表每个货物重量,再给定一个卡车的最大载货能力限制,问至少需要多少辆卡车(每辆载货能力为K)才能按顺序装完所有货物。" 反过来的问题是:"如果限定必须在D天内运完所有货物,求满足条件的最小运载能力。" 这种"求解一个最优阈值"的题,标准做法是二分答案加贪心检查:
def can_transport(weights, capacity, days): need = 1 cur = 0 for w in weights: cur += w if cur > capacity: need += 1 cur = w return need <= days def min_capacity(weights, days): left, right = max(weights), sum(weights) while left < right: mid = (left + right) // 2 if can_transport(weights, mid, days): right = mid else: left = mid + 1 return left注意二分初始边界:左边界是所有货物中的最大单件重量,因为单件货物不能拆开;右边界是总重量,因为一辆车拉完也算一种合法方案。很多同学在二分题上挂掉都是因为初始边界想当然,或者检查函数里没有重置临时变量。
2.6 并发和工程实现题:容易被忽略的一类
有些同学把所有精力压在纯算法题上,结果看到"实现一个带过期时间的LRU缓存""写一个多线程交替打印1到100"这类题就懵。字节后端研发岗对并发和工程能力是有要求的,编程题里出现这类题不奇怪。
这类题的核心不是奥数式思维,而是工程抽象能力。以"设计一个带过期时间的LRU缓存"为例,你要拆解成三件事:用哈希表做到O(1)查找,用双向链表维护访问顺序,再用一个时间戳字段来做惰性删除。Java同学可以用LinkedHashMap,Python可以用OrderedDict,但更重要的是向面试官说明你的数据结构和线程安全方案。
如果想单独练并发题,最快的路径是手写"两个线程交替输出奇偶数"和"生产者消费者"。这两种题代码量不大,但能把锁、条件变量、线程间通信这些基本概念串起来,面试手写很常用。
3. 通用解题套路:从读题到写码的四步法
题型背得再熟,上了考场还是需要一套稳的解题流程。我这些年总结了四步,写代码前先走完一遍,能显著降低翻车率。
3.1 第一步:根据数据范围反推算法
看到题先别急着写,先看输入规模。这是最重要的信息。这里给一个通用估算表:
| 数据规模 | 可接受的算法复杂度 | 常见算法 |
|---|---|---|
| n ≤ 10 | O(n!) / O(2^n) | 全排列、状态压缩DP、暴力回溯 |
| n ≤ 20 | O(2^n) | 状态压缩、回溯 |
| n ≤ 500 | O(n^3) | Floyd、三重循环DP |
| n ≤ 1000 | O(n^2) | 双重循环DP、常规双重遍历 |
| n ≤ 10^5 | O(n log n) | 排序、二分堆、线段树 |
| n ≤ 10^6 | O(n) 或 O(n log n) | 单调栈、哈希、线性扫描 |
| n ≥ 10^7 | O(n) 或 O(log n) | 前缀和、二分、简化模型 |
字节笔试的时间限制一般是1秒到2秒。如果在一次循环里再套一层循环,n是10^5,就是10^10次操作,基本必超时。先算这一步,可以避免写出一个"看起来正确但交上去超时"的方案。
3.2 第二步:从暴力出发找冗余
有时候最优解想不出来,不要放弃,先写一个暴力解。暴力解能帮你理清数据流向,同时大概率能拿一部分分。
写完暴力再问自己:哪里在重复算?比如连续子数组和,暴力每次都要做for i in range(l, r+1)的累加,这是典型的重复计算,用前缀和数组可以O(1)取区间和。再比如在一个字符串里反复查找某个字符是否存在,开一个计数数组就能把每次查找从O(n)降到O(1)。
这个思路很像整理房间:先把所有东西摊开,看到一堆重复劳动,再想办法用一个收纳盒集中解决。实际做题时,"先暴力后优化"能让你在面试讨论环节显得有章法,而不是上来就憋大招。
3.3 第三步:模板化写核心代码
很多算法题的框架是固定的,没必要每次现推。我建议你提前整理好自己的代码模板,考前多敲几遍。比如滑动窗口模板、二分模板、二叉树遍历模板、并查集模板、Dijkstra模板,全部整理成自己习惯的写法。
这里有一个不少初学者踩过的坑:模板背得太死,遇到变体不会调整。模板只是骨架,答题时一定要根据题目要求修改比较条件、返回值、边界。我喜欢把模板当成"脚手架"而非"标准答案",写代码时先套骨架,再把题目的个性化逻辑填进去。
3.4 第四步:用边界用例验证
写完代码不要立刻提交,花30秒到1分钟过一遍边界用例。我的检查顺序固定是:
- 输入为空/null
- 只有一个元素
- 所有元素相同
- 最大值/最小值(数值溢出)
- 负数和零
- 已经有序或完全逆序
- 目标值不存在
比方说二分查找,很多人死在没有考虑目标值不存在的情况。所以写完以后,手动在这个用例上走一遍循环,确认返回值符合预期。这个过程花时间少,但能帮你从"大概率AC"变成"稳稳AC"。
4. 那些"代码写得对但分不高"的原因
很多同学刷了很多题,正确率也不错,但笔试面试成绩却不理想。我观察下来,问题往往不在算法本身,而在几个容易忽略的细节上。
4.1 没有处理输入输出的边界
ACM模式下最常见的翻车点是输入读取。字符串可能带前后空格,数字之间可能有多个空格,行尾可能有回车。我见过有人用input().strip().split(" ")去切分,结果数据里连续两个空格,直接多出来一个空字符串,导致类型转换报错。
稳妥的做法是统一用input().split(),它会自动按空白字符切分,并把多余空格吞掉。如果一条输入有多个整数,用map(int, input().split())接收。输出时也要注意:多打印调试信息是大忌,在线判题只认标准输出,多输出一行print("debug"),这一题可能直接就0分了。
4.2 复杂度写炸了还不自知
有些同学写代码前不看数据范围,写完能跑就交,结果笔试报告里看到一片超时。要养成条件反射:看到10^5级别的数据,立刻判断你的主循环里有没有嵌套循环;看到10^6级别,尽量只做线性扫描或一次排序。
一个特别容易中招的场景是在循环体内调用str.count()、list.index()、in list这种O(n)操作,外层再套一个循环,整体就变成O(n^2)。要把"成员判断"尽早改成哈希表或集合查找。这一点在字符串处理题里尤其重要。
4.3 代码可读性差,面试官不想看
面试现场手写代码时,面试官会一行一行地看。变量名全叫a、b、c,函数逻辑全堆在一个30行的main里,哪怕思路正确,印象分也会大打折扣。
给自己定几条硬规矩:循环变量可以短,业务变量要见名知意;超过10行的逻辑块抽成小函数;关键步骤写一行注释说明"这一步在做什么"。字节的面试官普遍很看重代码工程化习惯,因为你入职后写的代码是要给团队维护的,代码风格本身就是工作能力的一部分。
4.4 不会和面试官沟通思路
面试和笔试最大的区别就是过程可见。面试官抛出题后,不要闷头就写,先花一两分钟讲思路:"我打算先预处理数据,然后用双指针维护一个窗口,每次移动右指针,如果条件不满足就收缩左边界,这样每个元素最多进出窗口两次,整体O(n)。" 这一段话说完,即使你代码写错了,面试官也知道你不是瞎写,而是在按一个可行思路推进。
如果中途发现思路不对,也千万别嘴硬。主动说"我刚想到一个边界情况,需要调整一下方向",比强撑着写完再被发现要好得多。面试官想看到的是候选人如何面对错误,而不是永远不出错的机器。
5. 30天备战计划:我的私人安排
备战春招,最怕的是没有节奏。我见过有人从早刷到晚刷了一个月,结果碰到新题还是没思路;也见过有人每天只刷3道,但每道都吃透,照样拿到offer。下面这个30天计划来自我带过的学员和我自己的刷题复盘,适合大多数研发方向的同学。
5.1 前两周:过完核心数据结构
不要一上来就刷题,先把地基打牢。两周时间可以这样分配:
- 第1-3天:数组、字符串、链表、栈、队列
- 第4-6天:哈希表、堆、二分查找
- 第7-9天:二叉树、递归、DFS/BFS
- 第10-11天:动态规划基础题目
- 第12-14天:图、并查集、拓扑排序(选做)
每天只做2-3道题,但每道题都要能讲清楚解题思路。遇到不会的,允许看题解,但看完必须自己重新写一遍,隔天再默写一遍。这一阶段的目标不是刷量,而是把数据结构的特性刻进脑子里。
5.2 第三到第四周:按题型集中刷题
这一阶段开始针对字节的高频题型做专项训练。每天一个专题:周一字符串哈希,周二双指针滑动窗口,周三二叉树DFS,周四DP,周五二分贪心,周六并发工程题,周日复盘本周错题。
每个专题至少连刷10道以上,整理出这个专题的通用套路。比如滑动窗口,你会发现所有题的解法都长得很像,只是"什么时候收缩左边界"不同。把不同点记下来,比盲目刷新题有用得多。
5.3 最后一周:全真模拟与错题复盘
最后一周不要再大量刷新题,重点做两件事:全真模拟和错题复盘。
全真模拟就是严格按笔试流程来:设好90分钟倒计时,3道题,关掉聊天工具,不允许看题解,模拟完再对答案。一天最多模拟一场,否则容易变成无效刷题。模拟完以后,把每道题卡住的原因记在错题本上,是读题慢?边界没考虑?还是某个套路不熟?
错题复盘不要只看代码,要写一句话策略,比如"数组子串和先想前缀和"、"树路径问题先想DFS带参数"、"遇到最大值最小化问题优先二分答案"。
5.4 刷题数量之外的三个建议
第一,不要迷信语言。Java和Python都可以,但必须把常用API背熟,比如Python的collections、heapq、bisect,Java的HashMap、ArrayList、PriorityQueue。考试时想不起来API很难受。
第二,练习脱离IDE自动补全。很多本地IDE的自动补全在笔试环境里是没有的,平时写代码尽量少依赖提示,多手敲。
第三,有条件的话,拿一个专门的时间段在在线判题平台上练ACM模式,学会自己写输入输出。这一步很多人忽略,真上了考场发现连从stdin读数据都要想半天,那就太冤了。
6. 现场考试时的操作细节:怎么多拿20分
实力到了,临场发挥也很重要。这里分享几个我在实战中总结出的操作细节,至少能帮你在原有水平上多争20分。
6.1 先跑通再优化,还是先设计再动手
我的建议是:如果没有把握一次写对,先按可行的方案写一个能跑的版本,哪怕复杂度不是最优。因为笔试是踩点拿分,只要不超时,正确解和最优解分数一样。不过面试场景不同,面试官更看重你的优化思路,所以面试时一定要先说最优方案的设计,再落手写。
如果时间非常紧,宁可写暴力解先把简单的测试用例跑通,也不要花30分钟憋一个最优解最后没写完。打比赛和考试一样,先拿到手的分才是真的分。
6.2 自测用例要怎么设计
写完代码后,自测用例别只拿题目给的例子跑。我习惯自己构造四类用例:最小输入、普通输入、边界输入、极端输入。
比如一道数组题,我会先测空数组,再测只有一个元素,再测全负数或全零,最后测一个10万量级的数据看看会不会超时。自测用例的价值不是验证正确性,而是逼自己把隐藏的边界暴露出来。很多人平时刷题正确率不错,一到笔试就挂,就是因为写的用例全部是"温和"的,从未触发过溢出和越界。
6.3 WA之后不要慌,按这个顺序排查
提交后返回WA,很多人第一反应是重新读题,或者怀疑平台有毛病,这是最浪费时间的方式。我给自己定的排查顺序是:
- 再读一遍题目,确认没有漏掉"按字典序输出""元素可能重复""结果可能为0"这类条件
- 检查输入输出格式,特别是ACM模式下是否多输出了调试信息
- 找到循环和递归的边界,手动跑一遍小样例
- 检查数组索引是否越界、哈希表初始值是否正确
- 检查浮点、溢出问题,必要时用
long或Python的整数不会溢出
按这个顺序走一遍,大部分WA都能在5分钟内定位。不要一上来就怀疑平台,平台偶尔有bug,但你更需要先证明自己的代码没问题。
6.4 时间分配:留出最后5分钟
三题90分钟的考试,我通常这样分配:第一题20分钟,第二题25分钟,第三题35分钟,最后10分钟检查。第五分钟时会快速看一眼第三题,如果读完30秒内没思路,我会先把第一题写了再回来看。这种"先易后难"的策略能保证你最稳的分数先落袋。
如果某一题卡了10分钟以上,立刻跳下一题。不要觉得可惜,笔试的时间成本很高,一题卡太久可能全局崩盘。
6.5 一些容易被忽略的小技巧
提前熟悉笔试题库网站的编辑器,特别是"运行"和"提交"的区别。有些环境的"运行"只是跑示例,不会判分,而"提交"才会进入正式判题。另外,代码里输出任何调试信息前都要注释掉,不然会被判成错题。
还有一个小习惯:开始写代码前,先把if __name__ == "__main__"和核心函数签名写好,再把输入读取写完,确保第一步就能跑起来。这样即使后面卡住,你至少有一个能处理的框架,而不是一堆零散代码。
最后再分享一个我自己的习惯:每次复盘编程题,不要只记题解,要记一句话策略。比如"数组子串和先想前缀和"、"树路径问题先想DFS带参数"、"最大值最小化先想二分"。到笔试前翻一遍,比再刷几十道新题更有用。希望大家都能把编程题这关稳稳拿下。