字节跳动2017秋招编程题盘点:四大高频算法考点全解析
前阵子帮一位学弟准备校招翻出了自己当年整理的一套字节跳动2017秋招编程题汇总。说实话字节的笔试风格这么多年一直很稳定——题面看着不复杂考点也非常基础但真正动笔写的时候处处是细节。很多人以为刷题就是拼手速、拼题量其实从字节这类大厂的真题里最能看出一家公司的筛选逻辑扎实的编码基本功、严谨的边界意识、以及快速把思路转换成代码的能力。这篇文章会以这套2017秋招编程题汇总为主线逐题拆解题目背后的考察点、解题思路、完整代码以及容易踩的坑。适合正在备战校招、尤其是目标是大厂算法岗/开发岗的同学也适合想检验自己编码基本功是否过硬的人。题目本身不算难但我会把每一道题都往深了讲包括变体、优化思路和现场笔试的应对策略争取让你看完之后再遇到同类型的题都能轻松拿捏。1. 2017年字节秋招编程题整体复盘1.1 这套题到底考什么先说个整体印象。2017年那会儿字节跳动还在快速扩张期校招笔试的命题风格已经很有“头条味”不考偏题怪题也不玩复杂的数学建模试卷上出现的全是面试里最高频的那几类基础算法。我梳理下来整套题大致覆盖了这么几个方向数组与动态规划比如连续子数组最大和这是一道LeetCode上被翻烂的经典题但出现在笔试里考察的是你能否在高压环境下快速写出既简洁又正确的解法。排序与数学思维比如数组中三个数的最大乘积考的是对负数情况的敏感度以及排序/扫描两种思路的取舍。字符串与模拟比如大数相乘这是面试官最喜欢用来考察代码实现能力的题因为它没有高深的算法但非常考验你对进位、下标、前导零这些细节的掌控。树与计数比如字典序第K小数字这道题既是搜索题也是思维题需要把字典序排序理解成对十叉树的深度优先遍历。从这套题的分布能看到一个很明显的信号字节看重的是程序员的基础算法能力、代码风格和调试能力而不是背诵了多少奇技淫巧。1.2 笔试环境与实战策略还有一点值得单独拿出来说2017年那会儿的在线笔试系统大多数是不支持本地IDE调试的代码要直接写在网页编辑器里编译报错也只能看到有限的日志。这意味着你在电脑上写代码的方式跟平时用IDE完全是两回事。我当时总结了一套应对策略放到今天依然适用先读全卷别急着做题。先花3-5分钟把四道题都扫一遍判断每道题的难度和熟悉程度确定做题顺序。先拿必拿的分。比如最大子数组和、三数最大乘积这种送分题必须一次写对不要在这种题上浪费过多调试验证时间。字符串大数相乘这种题思路很直白但容易在细节上翻车建议先写完主体逻辑再单独花时间检查边界。字典序第K小数字这种题如果一时半会没有思路先把暴力解法写出来保底再慢慢优化。这套策略帮我当年在限时笔试里稳住了节奏也推荐给大家参考。2. 高频考点一连续子数组最大和2.1 题目描述与思路推导先来看第一类高频题在不同版本的面经里都出现过核心描述是这样的给定一个整数数组 nums找到一个具有最大和的连续子数组子数组最少包含一个元素返回其最大和。例如输入 [-2,1,-3,4,-1,2,1,-5,4]输出 6因为连续子数组 [4,-1,2,1] 的和最大。如果第一次遇到这道题很多人的第一反应是暴力枚举。枚举所有起点 i 和终点 j把区间和都算一遍时间复杂度是 O(n²)而且严格来说如果还要算区间和会到 O(n³)。笔试里数组长度动辄几十万暴力解法基本就是等死。正确的思路是动态规划。核心思想很简单遍历数组时用一个变量 currentSum 记录以当前位置结尾的子数组的最大和用一个全局变量 maxSum 记录历史最大值。每次遇到一个新元素考虑两种情况之前的 currentSum 是正数那加上它是有益的继续累加。之前的 currentSum 是负数那与其拖着这个负累赘不如直接从当前元素重新开始。这个思路其实是一种贪心动向局部最优解可以递推到全局最优解。面试官非常喜欢这道题因为它能同时考察候选人是否理解DP的状态转移思想以及是否意识到空间可以压缩。2.2 代码实现与复杂度分析用C实现的话代码很简单几乎可以背下来int maxSubArray(vectorint nums) { int currentSum 0; int maxSum nums[0]; for (int num : nums) { currentSum max(num, currentSum num); maxSum max(maxSum, currentSum); } return maxSum; }时间复杂度 O(n)空间复杂度 O(1)。这是标准答案但我要提醒你一个细节maxSum 初始值要设为 nums[0]而不是 0。为什么因为如果数组全是负数比如 [-3, -1, -2]初始化为 0 的话最终会返回 0但正确答案应该是 -1。这个边界条件在笔试里非常容易翻车我当年就见过不少同学挂在上面。Java 版本同样简洁写法几乎一样public int maxSubArray(int[] nums) { int currentSum 0; int maxSum nums[0]; for (int num : nums) { currentSum Math.max(num, currentSum num); maxSum Math.max(maxSum, currentSum); } return maxSum; }2.3 易错点与变体拓展这道题除了最大和本身笔试里还经常出现两个变体顺便一起讲了。第一个变体是要求输出最大子数组的起始和结束下标。这个其实不复杂只需要在更新 currentSum 的时候判断它到底是重新从当前位置开始还是延续之前的区间。如果是重新开始就记录 start 临时变量每次更新 maxSum 的时候把最终的 left 和 right 记录下来。这个考点考察的是你是否真的理解了DP的递推过程而不只是背了个公式。第二个变体是最大子数组乘积。LeetCode 152题思路跟最大和类似但因为乘法有负负得正的特殊性需要额外维护一个最小值。每次更新时用当前元素、当前元素乘以前面的最大值、当前元素乘以前面的最小值三者取最大和最小。这道题在字节笔试里也出现过强烈建议一起练熟。还有一个容易忽略的点如果题目允许子数组为空那结果应该至少是 0。但大多数情况下题目要求子数组至少包含一个元素所以写代码前一定要先读清楚题面。3. 高频考点二数组中三个数的最大乘积3.1 题目描述与常见误区第二道题也是各大公司笔试的常客出现在2017年字节秋招的某个版本里题目如下给定一个整型数组在数组中找出由三个数组成的最大乘积并输出这个乘积。注意数组长度可能很大也可能包含负数。例如输入 [1,2,3]输出 6输入 [-100,-98,-1,2,3,4]输出 39200因为 (-100) × (-98) × 4 39200。第一次看到这题很多人直接排序后取最大的三个数相乘这是一个经典的错误示范。数组一旦包含负数两个绝对值很大的负数相乘会得到正数反而可能超过三个正数的乘积。经典例子就是 [-100, -98, -1, 2, 3, 4]排序后最大的三个数是 2、3、4乘积只有 24但 -100、-98、4 这三个数的乘积却是正的 39200。所以这题真正的考察点有两个第一你有没有考虑到负数的情况第二你能不能分析出正确的可能性组合。实际上三个数的最大乘积只可能来自两种情况最大的三个数相乘。最小的两个数可能为负数与最大的一个数相乘。为什么是这两种因为如果想让乘积为正且最大要么全正数取最大要么两个负数抵消后乘一个最大正数。其他组合比如一正两负其实已经被第二种情况覆盖三负乘积必为负不可能是最大值除非数组里只有负数但那种情况也得从这两种情况里找结果还是对的。3.2 两种主流解法对比解法一是排序。排序后直接取 max(nums[0] * nums[1] * nums[n-1], nums[n-3] * nums[n-2] * nums[n-1])。时间复杂度 O(n log n)代码非常简洁public int maximumProduct(int[] nums) { Arrays.sort(nums); int n nums.length; int case1 nums[0] * nums[1] * nums[n - 1]; int case2 nums[n - 3] * nums[n - 2] * nums[n - 1]; return Math.max(case1, case2); }解法二是不排序只做线性扫描。维护五个变量最大的三个数 max1、max2、max3以及最小的两个数 min1、min2。遍历数组时分别更新它们最后同样比较两种情况。时间复杂度 O(n)空间 O(1)。这种做法更适合在面试里展示你的优化意识。public int maximumProduct(int[] nums) { int max1 Integer.MIN_VALUE, max2 Integer.MIN_VALUE, max3 Integer.MIN_VALUE; int min1 Integer.MAX_VALUE, min2 Integer.MAX_VALUE; for (int num : nums) { if (num max1) { max3 max2; max2 max1; max1 num; } else if (num max2) { max3 max2; max2 num; } else if (num max3) { max3 num; } if (num min1) { min2 min1; min1 num; } else if (num min2) { min2 num; } } return Math.max(max1 * max2 * max3, min1 * min2 * max1); }说实话笔试里排序解法已经够用代码短不容易出错。但如果你去现场面试面试官追问“能不能不用排序”能顺手写出线性扫描版本会是很加分的表现。3.3 溢出与边界情况提醒这题还有一个隐藏的坑整型溢出。数组元素范围如果是 int且三个较大的 int 相乘结果可能超过 int 的最大值 2^31 - 1。网上不少参考代码直接用 int 存乘积遇到边界数据会出错。稳妥的做法是把乘积变量声明为 long。计算时先转成 long或者用 long 类型接收计算结果。比如排序版本的返回类型如果题目要求是 int那说明测试数据比较友好但如果题目没给明确范围我建议你直接用 long 计算后再转回需要的类型毕竟笔试系统的测试用例有时比想象中更狠。还有一种边界情况数组长度恰好为 3。这时无论走哪种情况结果都是唯一的那三个数相乘上面的代码天然支持。但注意如果你的线性扫描版本初始化值写错了比如把 max1 初始化为 0遇到全负数数组就会出错。这也是初始化值一定要用 Integer.MIN_VALUE / Integer.MAX_VALUE 的原因。4. 高频考点三字符串大数相乘4.1 为什么大厂爱考大数题第三类是字符串处理题字符串大数相乘在2017年字节秋招编程题里也出现过。题目描述很简单给定两个以字符串形式表示的非负整数 num1 和 num2返回 num1 和 num2 的乘积它们的乘积也表示为字符串形式。例如输入 123 和 456输出 56088。这道题最直白的考察点就是你的基本功。它不需要任何高级算法只要你用小学竖式乘法的思路把每一位相乘的结果累加到位数对应的位置上再把进位处理好就做完了。但它非常考验代码实现能力尤其是对下标、进位、前导零的处理。很多人平时用 BigInteger 用习惯了突然要求在字符串层面实现乘法会有点无从下手。这恰恰是笔试的意义所在它模拟了真实业务里无法依赖基础库函数时的场景比如处理超过 double 精度的大数计算或者做密码学相关算法时需要自己实现大数运算。4.2 竖式解法从模拟到优化大数相乘有多种做法从最简单的 O(n*m) 模拟到基于快速傅里叶变换的 O(n log n) 优化笔试阶段掌握前者就够了。核心思路是先定义一个长度为 len1 len2 的整型数组 result因为两个长度分别为 n 和 m 的数相乘结果长度最多为 nm。然后用两层循环遍历两个字符串的每一位将 num1[i] 和 num2[j] 的乘积加到 result 的对应位置上。这里有一个非常经典的下标技巧num1 的第 i 位从低位开始和 num2 的第 j 位相乘的结果应该累加到 result[ij] 和 result[ij1] 上。其中 result[ij1] 存当前位的值result[ij] 存进位。不过更常见的做法是先把乘积直接加到 result[ij1] 上最后统一处理进位顺序从低位到高位。我用C写一个可运行的版本string multiply(string num1, string num2) { int n num1.size(), m num2.size(); vectorint result(n m, 0); for (int i n - 1; i 0; i--) { for (int j m - 1; j 0; j--) { int mul (num1[i] - 0) * (num2[j] - 0); int p1 i j, p2 i j 1; int sum mul result[p2]; result[p2] sum % 10; result[p1] sum / 10; } } string ans; for (int num : result) { if (!(ans.empty() num 0)) { ans.push_back(num 0); } } return ans.empty() ? 0 : ans; }这段代码最核心的地方在于结果先累加到 p2 上再把进位加到 p1 上。如果你反过来先加 p1 再加 p2会很麻烦。用 p2 存个位、p1 存进位的方案会让进位处理非常干净。4.3 进位的两个隐藏陷阱第一前导零。当乘数中有 0 时比如 0 × 123result 数组全是 0。上面的代码通过ans.empty() num 0跳过了前导零最后直接返回 0。第二字符转数字的偏移。很多人在循环里忘记减 0直接把字符的 ASCII 码拿来相乘算出来一堆奇怪的结果而且很难排查。建议在循环开头先把两个字符串的每一位转成 int 数组可以省掉很多麻烦。比如int[] num1Arr new int[n]; for (int i 0; i n; i) { num1Arr[i] num1.charAt(i) - 0; }这也是我当时踩过的一个坑代码逻辑看着都对但结果怎么都不对最后才发现是 ASCII 码的问题。字符 9 的 ASCII 是 573 是 51两个相乘是 2907而不是 27当然全乱了。如果笔试时间充裕还可以考虑用分治发来优化大数相乘也就是 Karatsuba 算法但2017年那场笔试的测试数据规模不大竖式模拟就足够了。如果志在面试中展示更强的算法能力Karatsuba 可以作为扩展知识了解一下。5. 高频考点四字典序第K小数字5.1 题目描述与暴力解法第四道题是整套题里最有区分度的一道也是我认为含金量最高的一道题目如下给定整数 n 和 k返回范围 [1, n] 中按字典序排序的第 k 小的数字。例如 n 13字典序排列为 1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9第 2 小的数字是 10。看到“字典序”三个字第一反应是把 1 到 n 的所有数字转成字符串然后用字符串排序再取第 k 个。这样做确实能得到正确结果但时间复杂度是 O(n log n)空间 O(n)。当 n 达到 10^9k 很大时直接内存爆炸、超时没商量。这时候需要换一个角度理解字典序。字典序排序的数字实际上构成了一棵十叉树根节点是 1 到 9每个节点下面有 0 到 9 十个孩子节点。比如 1 的孩子是 10、11、12……一直到 1910 的孩子又是 100、101……。我们需要找的就是在这棵十叉树上按前序遍历顺序走到第 k 个节点。这个思路一下子就打开了。问题从排序问题变成了树的遍历问题而树的遍历不需要把所有节点都生成出来只需要通过数学计数跳跃式前进。5.2 十叉树计数法详解核心子问题是给定前缀 prefix在 [1, n] 范围内以 prefix 为前缀的数字一共有多少个比如 n 123prefix 1。以 1 为前缀的数字有 1, 10~19, 100~123一共 1 10 24 35 个。注意当节点本身超出 n 的范围时需要截断计算。计算逻辑不复杂cur prefix next prefix 1 count 0 while (cur n) { count min(next, n 1) - cur cur * 10 next * 10 }初始时 cur prefixnext prefix 1。count 累加的是一层里前缀为 prefix 的数字个数。cur 和 next 分别乘以 10 进入下一层直到 cur n 结束。min(next, n1)是对最后一层可能被 n 截断的情况做保护。接下来是主逻辑。从 prefix 1 开始先计算以 1 为前缀的子树节点数 count如果 k count说明第 k 个数不在 1 这个子树里要做的是 prefix同时 k - count跳到下一个兄弟节点。如果 k count说明第 k 个数就在当前前缀子树里此时 prefix * 10同时 k--进入下一层继续判断。k-- 是因为当前 prefix 本身已经占了一个位置。循环直到 k 0此时 prefix 就是答案。5.3 代码实现与调试技巧Java 实现如下public int findKthNumber(int n, int k) { int prefix 1; k--; while (k 0) { long count count(prefix, n); if (k count) { k - count; prefix; } else { prefix * 10; k--; } } return prefix; } private long count(long prefix, int n) { long cur prefix; long next prefix 1; long count 0; while (cur n) { count Math.min(next, (long)n 1) - cur; cur * 10; next * 10; } return count; }这里有一个特别重要的细节count 函数和 prefix 变量要用 long 类型。因为当 prefix 是 10^9 级别乘 10 之后会溢出 int。我当年第一次写的时候prefix 用 int跑到深处直接溢出变成负数结果死循环调试了很久才发现问题。另一个调试技巧可以先实现一个暴力版n 小的时候用暴力结果和优化版本对拍确保优化逻辑没问题再提交。我在本地测试时经常这么干特别是面对这种数学推导型的题目对拍能省下大把人工验证时间。还有一点这道题在LeetCode上是第 440 题很多题解都用递归或迭代实现但思路是一样的。建议练完这道题之后顺手把 LeetCode 386字典序排数也做一遍那道题要求输出全部字典序排列正好可以巩固十叉树的遍历思路。6. 实战复盘与避坑指南6.1 笔试现场的时间分配建议拿到这套题先别急着写第一题。我见过太多人埋头做完第一题发现自己用了40分钟后面简单题反而没时间写。笔试考的不只是会不会还有会不会取舍。我的建议是这样分配前5分钟通读全部题目给每道题标记难度和自己的熟悉度。按“熟悉的简单题 → 有思路的中等题 → 需要思考的难题”顺序做题。每道题最多留25-30分钟超过时间立刻换成保底解法或者跳过。留最后10分钟做全局检查。重点检查边界条件比如数组长度是否为 1、空字符串、负数、0、溢出等。以这套2017年的题为例理想顺序是先做三数最大乘积再做连续子数组最大和然后是大数相乘最后攻坚字典序第K小数字。这样前面三道题拿满最后一道即使拿不到满分整体分数也不会难看。6.2 常见失分点速查表我根据当年在网上的讨论和自己带学弟学妹过程中总结的常见失分点整理了一张速查表题目类型高频失分点解决办法连续子数组最大和maxSum 初始化为 0初始化为 nums[0]三数最大乘积只考虑三个最大正数比较两组情况并防溢出字符串大数相乘字符未减去 0先转 int 数组字符串大数相乘未处理前导零构建结果时跳过开头为 0 的位字典序第K小数字变量类型溢出用 long 保存 prefix 和 cur所有题目未考虑空输入先判空返回默认值这张表是我最喜欢的复盘方式。每次笔试或模拟面试后把错题和错误原因都归纳到表里下次考试前看一眼远比重新刷一百道题有效。6.3 从编程题反推面试准备策略最后聊一点更深的东西。字节这套2017秋招编程题表面上只是四道算法题但它的出题内涵很值得玩味。它没有像有些公司那样出特别复杂的后缀自动机、网络流或者动态规划优化而是选了这些“基础中的基础”。这说明字节的筛选逻辑是宁可要一个能把简单题写到无懈可击的候选人也不要一个能把难题写出来但边界漏洞百出的人。所以备战校招我强烈建议你这样做把剑指offer和LeetCode Hot 100里的基础题吃透尤其是数组、字符串、链表、二叉树、动态规划这五类。每道题都刻意练习“一遍写出无 bug 代码”因为笔试没有不断调试的时间。练题时注意时间复杂度优化和空间复杂度优化并能口头解释为什么这么写是最优的。多参加模拟笔试尤其是用牛客网或者公司官方笔试平台熟悉不依赖本地IDE的编码环境。我当时练到后面给自己定了一个标准简单题必须在5分钟内写完并一次性通过所有用例中等题15分钟难题30分钟出思路。这个标准帮助我在真正笔试时即使遇到没见过的题也不会因为紧张而乱套。这套2017年字节跳动秋招编程题虽然已经过去好几年但里面的每道题至今仍然是各大厂笔试、面试的高频题。认真刷一遍再按上面的思路复盘一遍你会发现自己对基础算法的理解会扎实很多。我个人最大的体会是面试官看重的往往不是你能做出多少道难题而是你能不能把每一道简单题做到滴水不漏。把这四道题彻底吃透比盲目刷两百道新题更有价值。如果后面有时间可以再用同样的方式整理一下同一年其他大厂的笔试题目方便横向对比不同公司的出题风格。