字节跳动秋招笔试复盘:算法与计算机基础的系统梳理
2026/8/30 6:31:42 网站建设 项目流程

又是一年秋招季,整理电脑里的旧文件时翻出了2017年的笔试记录。字节跳动那套秋招开发工程师笔试试卷,是我当年印象最深的一套题。那时候字节跳动的校招规模还没现在这么大,但笔试风格已经非常鲜明:不堆冷门记忆题,不考纯概念背诵,而是把算法、数据结构和计算机基础揉在一起,用几道题去测你的真实功底。

这套卷子带给我的最大收获,不是最后拿到什么结果,而是它逼着我把操作系统、计算机网络、数据结构这些基础课重新过了一遍,搞清楚了很多以前只记结论、不知道推导过程的东西。这篇文章就结合当时试卷的常见题型和核心考点,聊聊我是怎么拆题、怎么分析、怎么踩坑的,如果你是准备校招的应届生,或者想系统巩固基本功的在职开发,可以参考一下我的思路。

1. 先看懂这套试卷:2017秋招笔试到底在考什么

1.1 试卷的大致构成与题量分布

先说整体结构。字节跳动2017秋招开发工程师笔试是典型的在线OJ形式,时长大约2小时。根据当时参加过的同学反馈和我自己的记录,试卷大致由两部分组成:一部分是客观选择题,另一部分是编程题。选择题通常在20道左右,覆盖数据结构、算法、操作系统、计算机网络、编程语言基础;编程题一般是2到3道,以算法题为主,难度有明显梯度,第一题偏热身,后面逐渐加大难度。

题型数量考查方向建议用时
单选/多选20道左右数据结构、操作系统、网络、语言细节40分钟
编程题2-3道哈希、滑动窗口、动态规划、拓扑排序等80分钟

从题目安排能看出一个特点:选择题考的是“你是否真正理解”,编程题考的是“你能不能落成代码”。两者加在一起,既筛理论功底薄弱的人,也筛动手能力不足的人。

需要说明的是,校招笔试卷子通常不会原封不动对外流出,所以我这里讲的是基于当时考生回忆和同类岗位常见考点的还原,科目类型和考察思路是准确的,具体题目细节属于综合归纳。这样梳理反而比死记一套原题更有价值。

1.2 为什么是这个出题思路:筛的不是背过面经的人

很多人拿到这套卷子第一反应是:“怎么没有XX框架的题?怎么不考XXX技术栈?”如果这么想,就误解了笔试的目的。2017年字节跳动研发岗笔试的核心定位是——用最少的题目,判断一个应届生在计算机基础上能打多少分。

算法题之所以占比这么高,是因为它在短时间内容易暴露一个人的思维习惯。你是直接写暴力解,还是能快速想到优化方向?你的代码边界处理是否严谨?你对复杂度的敏感度如何?这些能力靠背诵面经是装不出来的。选择题则负责检验基础知识点是否形成体系,比如TCP的状态流转、操作系统的页面置换,这些是写业务代码时不一定天天碰到、但出了问题必须能快速定位的东西。

我当时做完这套卷子最大的感受是:它不追求题目偏门,而是把教科书中“看似简单”的知识点挖得很深。所以备考的核心不是海量刷偏题,而是把每个高频考点的原理彻底弄明白。

2. 选择题里的硬核基本功:每一分都得有理有据

2.1 数据结构与算法选择:从二叉树到排序的经典陷阱

选择题里数据结构占比很高,而且经常出“看起来不难,但容易错”的题。

举个例子,完全二叉树的题目反复出现。题目可能是:一棵完全二叉树有n个节点,问叶子节点有多少个。很多人凭感觉直接答n/2,这是不对的。完全二叉树的叶子节点数需要根据最后一个节点的位置来判断。如果n是偶数,最后一个分支节点是n/2,叶子节点数是n/2;如果n是奇数,叶子节点数是(n+1)/2。简单验证一下:一棵只有1个节点的完全二叉树,叶子数是1,按奇数公式算就是(1+1)/2=1;一棵有2个节点的完全二叉树,叶子数是1,按偶数公式算就是2/2=1。如果你只是背公式,换个参数就可能搞混。我在考场上就因为这题犹豫了很久,后来发现关键不是背公式,而是去理解“完全二叉树按层序遍历编号后,父节点和子节点的编号关系”:编号为i的节点,左孩子是2i,右孩子是2i+1。知道了这个关系,很多二叉树题目都能现场推出来,不需要背结论。

排序算法也是选择题的重灾区。快速排序在什么情况下退化到O(n^2)?当时卷子上有一道题问:对已经有序的数组使用快速排序,且每次都以第一个元素作为枢轴,时间复杂度是多少。答案是O(n^2),因为每次分区都极度不平衡,一侧为空,递归深度达到n。这个知识点本身不难,但它背后牵出一个工程实践问题:生产中绝不会用这种朴素快排,而是会用三数取中、随机化枢轴、小区间插入排序等优化手段。这些在笔试卷子里不一定直接考,但如果你理解到位,遇到“如何优化快排最坏情况”这种问答,就能答得比别人深入。

注意:选择题里一旦出现排序、二叉树、哈希相关的题目,先确认题目给定的约束条件,比如“最坏情况”“平均情况”“稳定/不稳定”。这些限定词往往才是真正的考点。

2.2 计算机网络与操作系统:背八股翻车的重灾区

网络和操作系统选择题是拉开分差的地方。因为这两块知识点多而杂,很多人复习的时候靠“背题”,结果换个问法就露馅。

TCP四次挥手是我记忆中几乎必考的内容。题目不是简单问“有几个阶段”,而是问:主动关闭方在发送最后一个ACK之后进入什么状态?这个状态要持续多久?为什么?答案是TIME_WAIT,持续2MSL(最大报文段生存时间),原因是确保最后一个ACK能到达对方,如果丢失可以重传,同时让旧连接中的延迟报文段自然消失,避免干扰新连接。很多人只记住了TIME_WAIT这个名字,却说不清为什么是2MSL。这个问题如果换成“为什么主动关闭方要等2MSL而不是1MSL”,估计能筛掉一半人。

操作系统部分,页面置换算法很常考。LRU和FIFO的缺页次数对比是经典题目。但2017年这套卷子更深入一点,我记得有一道题是关于LRU算法在实际实现中用什么数据结构:哈希表加双向链表,哈希表负责O(1)查找,双向链表负责O(1)删除和移动。这其实已经是在考察“你是否知道算法落地时怎么设计”。如果你只是背了LRU的概念,这题就没办法靠猜。

还有一道关于进程和线程的题也让我印象很深:进程和线程在哪些资源上是共享的,哪些是独立的?线程共享进程的地址空间、文件描述符、信号处理器等,但每个线程有自己的栈空间和寄存器上下文。这题的干扰项通常会设置成“线程拥有独立的地址空间”,这是错的。只要理解了“线程是调度的基本单位,进程是资源分配的基本单位”,这题就不会错。

2.3 语言与工程基础:边界、内存、异常处理

2017年的Java/C++岗位笔试试卷里还会涉及一些语言细节题。比如Java的HashMap在JDK 1.8中是如何解决哈希冲突的?答案是链地址法加红黑树:当链表长度超过8且数组容量大于等于64时,转化为红黑树。这个题到今天仍然是高频考点。

C++方向则喜欢考析构函数为什么通常声明为虚函数。因为当基类指针指向派生类对象,delete操作时如果析构函数不是虚函数,就不会调用派生类的析构函数,导致资源泄漏。这个知识点考察的是“多态在析构场景下的应用”。

这类型题目对工程经验不丰富的应届生来说有一定难度,所以我的建议是:不要在语言细节上花太多时间死磕,而是把最常见、最影响实际开发的点弄透。比如数组越界、内存泄漏、空指针这些,一定要能说出“为什么危险”和“如何避免”。

3. 编程题解析:从暴力解到最优解的全过程

3.1 编程题出题风格:看似熟悉,实际全是套路

字节跳动2017秋招笔试的编程题,给我的整体感觉是:题目背景简单,没有复杂的业务场景,但解法有层次,暴力解能拿部分分,最优解需要动脑子。

常见的题型包括:

  • 哈希表应用类:找两数之和、判断是否存在重复元素。
  • 滑动窗口类:最长无重复子串、最小覆盖子串。
  • 动态规划类:最长递增子序列、编辑距离、背包问题变体。
  • 图论类:拓扑排序、单源最短路。

考场上时间有限,不可能每道题都从零开始推导。所以我自己做题有个固定流程:先看数据范围,判断该用O(n^2)还是O(n log n)还是O(n);再看能不能用双指针或哈希表降低复杂度;最后才考虑动态规划。如果数据范围是10^5,就基本别想O(n^2)的暴力解了。

3.2 完整推导:滑动窗口求最长无重复子串

这道题可以算是这类笔试的经典常客。题目描述是:给定一个字符串s,找出其中不含有重复字符的最长子串长度。

最直观的解法是暴力枚举所有子串,逐个检查是否有重复字符。时间复杂度O(n^3)或者O(n^2),取决于检查方式。这个解能拿一点分,但肯定不是出题人想要的。

优化思路是用滑动窗口加哈希集合:

def lengthOfLongestSubstring(s: str) -> int: char_set = set() left = 0 res = 0 for right in range(len(s)): while s[right] in char_set: char_set.remove(s[left]) left += 1 char_set.add(s[right]) res = max(res, right - left + 1) return res

这个解法的时间复杂度是O(n),空间复杂度是O(min(n, 字符集大小))。思路其实很简单:右指针不断向右扩展,把字符加入集合;一旦发现当前右指针指向的字符已经在集合里,就移动左指针,把左指针对应的字符移除,直到集合里没有重复字符。每次更新子串长度。

我当时第一次做这道题时写的是暴力解,能跑通小数据,但遇到长字符串就超时。后来整理错题才理解了滑动窗口的精髓:它利用了子串连续性这个特性,让左右指针都不回退,所以总移动次数是2n,复杂度是线性的。

如果面试官继续追问“能不能再优化”,可以提“用哈希表记录每个字符最近一次出现的位置”,这样左指针可以直接跳到重复字符的下一个位置,不需要逐格移动:

def lengthOfLongestSubstring(s: str) -> int: index_map = {} left = 0 res = 0 for right, ch in enumerate(s): if ch in index_map and index_map[ch] >= left: left = index_map[ch] + 1 index_map[ch] = right res = max(res, right - left + 1) return res

这个版本在重复字符较少时效率更高,而且代码简洁很多。笔试和面试中,能主动给出这个优化版本,会是一个加分项。

注意:滑动窗口类题目的关键是“窗口内元素满足某个条件”。如果条件不满足,就缩小窗口;满足就尝试扩大窗口。搞清楚左指针什么时候移动、移动多少,比死记代码重要得多。

3.3 完整推导:拓扑排序加字典序优先的变体

另一道让我印象深刻的编程题是一道任务调度题。大意是:给定N个任务和M个依赖关系,每个依赖关系表示某个任务必须在其前置任务完成后才能执行,要求输出一种合法的任务执行顺序。如果有多种合法顺序,要求输出字典序最小的那个。

这个题的本质是有向无环图的拓扑排序。最标准的解法是Kahn算法:先统计每个节点的入度,把入度为0的节点加入队列,每次从队列中取出一个节点输出,然后把它的所有后继节点入度减1,如果后继节点入度变为0,就加入队列。这个过程持续到所有节点都输出为止。如果最终输出节点数小于N,说明图中有环,不存在合法的拓扑序。

但加上“字典序最小”这个条件后,就不能用普通队列了,需要用优先队列(最小堆):

#include <vector> #include <queue> #include <functional> using namespace std; vector<int> topoSort(int n, vector<vector<int>>& edges) { vector<int> indegree(n, 0); vector<vector<int>> graph(n); for (auto& edge : edges) { int a = edge[0], b = edge[1]; graph[a].push_back(b); indegree[b]++; } priority_queue<int, vector<int>, greater<int>> pq; for (int i = 0; i < n; i++) { if (indegree[i] == 0) pq.push(i); } vector<int> result; while (!pq.empty()) { int cur = pq.top(); pq.pop(); result.push_back(cur); for (int nxt : graph[cur]) { indegree[nxt]--; if (indegree[nxt] == 0) pq.push(nxt); } } if ((int)result.size() != n) return {}; return result; }

为什么用优先队列?因为普通队列是先进先出,只能保证按下标或入队顺序输出,没法保证字典序。而最小堆每次取当前可选任务中编号最小的那个,就能在“拓扑序合法”的前提下做到字典序最小。这里的复杂度是O((N+M)logN),其中N是任务数,M是依赖关系数,logN来自优先队列的调整开销。

这道题给我的启发是:很多算法题都是在经典算法的基础上加一个约束条件,本质上是看你能不能把经典算法灵活改造。如果你只背过裸的拓扑排序,没理解队列在这里的作用,遇到“字典序最小”就慌了。这也是为什么我一直强调学算法要理解思路不是记住代码。

4. 笔试现场的经验与翻车记录

4.1 时间分配:选择题别恋战

我当年第一次做在线笔试时,犯了一个经典错误:在选择题上耗了太多时间。有一道网络题我拿不准,反复纠结了快10分钟,结果后面编程题时间不够,第一题写完暴力解,第二题只写了一半。后来我总结的考场原则是:选择题每题最多2分钟,没有思路就先标个最可能的答案,跳到下一题。编程题才是拿分的大头,一道完整的最优解抵得过好几道纠结的选择题。

合理的安排是:拿到卷子先花3分钟把所有题目扫一遍,对编程题的难度有数——哪题是热身、哪题是大题。然后先把简单编程题解决,再回头做选择题,最后集中火力攻难题。因为选择题是一锤子买卖,做完了不会因为你后面想起来改答案而自动加分,但编程题多写几个测试用例验证一下,能大幅提高通过率。

4.2 读题与边界条件:AC不了往往不是算法问题

在线笔试平台对编程题的评判是全自动的,任何边界条件没处理好都会导致运行错误或超时。我见过太多人算法思路对了,却因为没考虑空输入、没处理数据越界、没注意整型溢出而丢掉大量分数。

几个常见的边界坑:

  • 空数组、空字符串:很多解法在输入为空时会报错,必须单独处理。
  • 数组越界:循环里访问nums[i+1]前要先判断i+1是否在范围内。
  • 整数溢出:两个很大的int相加可能溢出,该用long long的地方别省。
  • 图可能不连通:拓扑排序、DFS时需要检查所有节点是否都被访问过。
  • 输入可能有重复数据:哈希表的插入和查找要留意覆盖关系是否正确。

我在写完每道编程题后都会花1分钟跑几个特殊用例:空输入、全相同元素、最大数据量的情况、只有一个元素的情况。这个习惯让我避免了好几次无谓的扣分。

4.3 复盘与一题多解:面试官真正想听的是什么

笔试结束后的复盘比刷题本身更重要。我当时花了几个晚上,把每道错题重新推导了一遍,尤其是那些“看答案能看懂、自己写就卡壳”的题目,我会关掉答案重新写,直到能流畅完成。

还有一个提升很大的习惯:对同一道题尝试多种解法。比如最长无重复子串那题,先写暴力解,再写滑动窗口,再写哈希表优化版。这个过程的收获不是“多记了一个解法”,而是理解了不同解法之间的复杂度差异从何而来,以及什么场景下应该选择哪一种。笔试之后如果通过,面试官很可能会围绕笔试题追问——为什么这么解?能不能再优化?有没有其他思路?如果只是背答案,这一环节很容易露怯。

5. 2017年的试卷,对今天校招复习的参考价值

5.1 从2017到如今的笔试趋势变化

七八年过去,校招笔试题型一直在变,但底层逻辑没变。现在的算法题难度整体有所提升,题目场景也更丰富,比如开始结合大数据处理、流式计算等背景。但核心还是那几类:数据结构、搜索与图论、动态规划、字符串处理。2017年这套试卷中暴露出的“重基础、重推导、重边界”导向,到今天依然适用。

另外,现在很多公司在线笔试平台会实时记录你的代码编译次数、测试用例通过率,甚至有时长指标。这意味着“一次编译通过”逐渐成为加分项。怎么提升一次通过的准确率?就是靠平时写题时的严谨性,不要依赖编译器反复帮你查错。

5.2 如何借这套题做系统复习

如果你想用这套题来检验自己的基本功,我建议按下面这个顺序过一遍:

  • 数据结构:数组、链表、栈、队列、哈希表、树、堆、图,每一个都要知道典型操作的时间复杂度。
  • 算法:排序、二分、双指针、滑动窗口、DFS/BFS、回溯、动态规划、拓扑排序、最短路。
  • 计算机基础:TCP/UDP、进程线程、内存管理、文件系统。
  • 语言基础:你用的主力语言的核心容器类底层原理、内存管理机制。

每过完一个小模块,就找10到15道对应类型的题做专项训练,不看题解,先独立思考30分钟,实在没有思路再看解析。然后用表格记录每道题的错因:是思路问题、边界问题,还是语法问题。这样做一周左右,再拿一套模拟题做整体测试,你就能明显感觉到自己的变化。

这几年我陆陆续续参与过一些新人面试,发现一个现象:很多候选人在算法刷题上投入了大量时间,但问他“为什么用哈希表而不用数组”“为什么这个解法是O(n)”时,却说不出所以然。反而是那些能把一道经典题从暴力解到最优解讲得清清楚楚的人,哪怕笔试分数不是最高,最终通过率却很高。归根结底,面试官想找的不是一个刷题机器,而是一个有扎实基本功、能解决实际问题的人。

回看2017年那套笔试试卷,它给我最大的启发就一句话:基础不牢,地动山摇。不管技术栈怎么换、框架怎么变,操作系统、网络、数据结构、算法这些底层能力,永远是最值得投入时间的部分。如果你正准备秋招,与其焦虑题目难不难、通过率高不高,不如静下心来把每一个高频考点的原理搞透,把每道经典题从暴力解到最优解完整推一遍。这套功夫下去了,分数是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询