字节跳动2017客户端工程师实习生笔试题,我当年是真刀真枪考过的。那会儿今日头条已经火到不行,身边投客户端实习岗位的同学一大片,笔试链接发过来的时候我还挺紧张。整场考试90分钟,Web编辑器,没有IDE提示,没有自动补全,选择题和编程题混在一起,时间卡得让人喘不过气。现在回头看,这场笔试考察的东西其实非常有指向性:基础扎实不扎实、代码习惯好不好、现场抗不抗压,基本一测就知道。这篇文章就把我当时遇到的题型、踩过的坑、后来复盘总结的考点,系统性整理出来,适合正在准备大厂客户端方向实习笔试的同学参考。
1. 2017年字节跳动客户端实习生笔试全景
1.1 招人逻辑:什么样的实习生能过笔试
2017年,字节跳动的客户端团队扩张得非常猛。今日头条App当时已经有几亿用户,内容资讯、视频、社交功能在频繁迭代,客户端是离用户最近的一层。团队招实习生,不是招来“学习”的,而是希望尽快上手写业务代码。所以笔试不会出特别偏的题,但会把基本功测得很透,因为移动端不像后端那样有海量并发请求,但对内存占用、页面渲染、网络请求、数据持久化都必须特别敏感,这些能力必须靠基础功底撑起来。
从笔试设计角度看,它要筛选的是“能写代码的人”,而不是“背过八股文的人”。所以选择题大部分是概念辨析,编程题则考察你能否在没有任何提示的环境下,把思路转成语法正确的代码。很多同学到了这种环境就露馅了:平时在IDE里靠快捷键和自动补全写代码,一旦换成简陋的在线编辑器,连StringBuilder都可能拼错。这一点恰恰是出题人想看到的,他们要的是离开工具之后依然稳的人。
还有个细节,2017年移动端招人明显比后端更看重操作系统和网络基础。原因很简单,客户端工程师每天都要和线程调度、内存管理、网络请求打交道,如果连进程线程、TCP这些概念都含糊,后面做性能优化时基本寸步难行。所以笔试里操作系统和网络的分值占比一点都不低,很多人栽在低估这部分上。
1.2 题型结构与整体考察目标
当年的在线笔试平台一般是牛客网或者赛码网,考试界面很简单,一个代码编辑器、一个倒计时、一排题目列表。整体结构大致是这样:
| 题目类型 | 大致题量 | 核心考察点 | 建议用时 |
|---|---|---|---|
| 不定项选择题 | 20道左右 | Java/C++、数据结构、操作系统、Linux、网络 | 25分钟 |
| 编程题 | 2-3道 | 编码能力、算法思维、边界处理 | 55分钟 |
| 简答题 | 0-1道 | 客户端机制理解(如Handler) | 10分钟 |
选择题里最烦人的是不定项选择,多选、漏选都不得分,有时候一道题四个选项里三个半都对,就是有一个模棱两可的干扰项,专门坑那种“好像在哪见过但记不清”的人。编程题通常第一题偏简单,后面开始上强度,考的核心不是你会不会某道LeetCode原题,而是你能不能把常见算法在限定时间内写对、写完整。
这里多说一句:笔试不是看你刷了多少题,而是看你在紧张环境下能否稳定输出。我当时有个朋友刷了300多道LeetCode,结果笔试第一道链表题就卡在边界条件上,最后只AC了一道。反而是另一个刷题量不到他一半的同学,三道题全都跑通。差别就在代码习惯,尤其是边界条件的处理,这一点后面会重点展开。
2. 核心考点逐个拆解:从选择题到编程题
2.1 Java/C++基础:数组和指针是重灾区
每年Java/C++选择题里,“数组和指针”永远是重灾区,字节2017年的笔试也不例外。当年考过的几类典型题型,我到现在还记得很清楚:
第一类是sizeof问题。比如int a[5]; int *p = a;,sizeof(a)是20字节(假设int占4字节),而sizeof(p)是8字节(64位系统下指针大小)。很多人想当然觉得“数组名不就是指针嘛”,一写答案就错。实际上数组名在大多数表达式中会退化为指针,但在sizeof里不会,这是一个极其经典的考点。
第二类是指针加减。比如int a[5] = {1,2,3,4,5}; int *p = a;,问*(p+3)是多少。答案是4,也就是a[3]。这里容易错的是把p+3理解成“地址加3个字节”,实际上指针加法是按指向类型的大小来移动的,int型指针加1移动4个字节。
第三类是二维数组的指针。int (*p)[4]是“指向含4个int元素的一维数组的指针”,而int *p[4]是“一个含4个int指针的数组”。这个区别在选择题里出现频率极高,只看声明就能劝退一批人。
Java部分的常考内容相对固定:HashMap的底层结构(数组+链表/红黑树以及put流程)、==和equals的区别、String/StringBuilder/StringBuffer的区别、final/finally/finalize的区别、try-catch-finally中return的执行顺序。其中finally那题我印象很深,因为看起来简单,实际很容易掉坑:
try { return 1; } finally { System.out.println("finally"); }这段代码会先执行finally块中的输出,然后才执行return 1。但如果是这样写:
try { return 1; } finally { return 2; }那最终返回的就是2,因为 finally 的 return 会覆盖 try 中的 return。这种题目放在选择题里,考察的就是你对 JVM 字节码执行顺序的理解,死记硬背很容易在变体题上翻车。
注意:在线笔试没有IDE提示,写完代码只能靠肉眼检查。数组和指针相关的代码,务必在纸上把内存布局画出来,能大大降低出错率。
2.2 数据结构与算法:编程题的套路与变体
2017年字节客户端笔试的编程题,基本可以归纳到几类:字符串处理、链表操作、二叉树遍历、经典动态规划。这些题目在LeetCode上都有原型,但笔试里往往加了一些细节变体,防止你直接背答案。
字符串处理是常客,最长不重复子串、字符串压缩、括号匹配、翻转单词顺序都是高频题。考字符串的核心原因,是它天然能考察你对索引、边界、字符编码这些细节的敏感度。比如“翻转字符串中的单词顺序”,很多人的第一版代码会在开头和末尾有多余空格时出错。
链表操作同样高频,反转链表、判断链表是否有环、两两交换相邻节点、合并两个有序链表,都是需要默写的题目。链表题得分的关键不是思路,而是画图。只要把指针的变化画出来,代码就不会写错。
二叉树题目中,层序遍历、最大深度、最近公共祖先是最常出现的。层序遍历需要借助队列,这个倒不难,但要注意题目要求的是按层输出二维数组还是一维数组,这决定代码的写法。最近公共祖先那道题则要理解递归的返回值语义,很多人在这里卡住。
动态规划在客户端笔试里不会考特别难的题,爬楼梯、打家劫舍、最长上升子序列这类经典题就够了。但要注意,笔试环境里动规题往往有几个隐藏陷阱:数组长度可能为0、数字可能为负数、结果可能溢出int范围。很多LeetCode跑得通的代码,到了笔试判题系统里就因为这些问题超时或报错。
这里我得说个经验:编程题不光看对不对,还看复杂度。比如“求滑动窗口最大值”这道题,你用暴力解法,时间复杂度是O(n*k),数据一大就超时。考场上如果只追求“能跑出结果”,很可能在最后一个测试用例上卡死。所以写代码之前先估算一下数据范围,再决定用什么算法。
2.3 操作系统、Linux与网络:客户端不能丢的地基
这套题里,操作系统和网络的分值占比出乎意料地高。进程和线程的区别、死锁的四个必要条件、虚拟内存的作用、页面置换算法,这些都是选择题和简答题的常客。尤其是死锁四条件——互斥、持有并等待、不可剥夺、循环等待,几乎每次笔试都会出现,只是换着花样考。
TCP/IP部分,三次握手、四次挥手、为什么TIME_WAIT要等2MSL,是经典的“老八股”问题。客户端工程师更需要理解这些,因为App的网络请求库底层全是TCP和HTTP。我记得有一道题问“HTTP和HTTPS的区别”,选项里有一个是“HTTPS使用对称加密传输数据”,这个说法其实不全对,因为HTTPS握手阶段用的是非对称加密协商密钥,之后的数据传输才用对称加密。这种选项就是专门用来淘汰“一知半解”的人的。
Linux基础也会考几道,比如查看进程用什么命令(ps)、查看端口占用用什么命令(netstat)、查看磁盘使用情况用什么命令(df)。如果你是客户端方向,还可能遇到“如何查看Android进程的CPU占用”这类结合题。客户端可能不太常用Linux,但这些命令搞懂了,后面做性能分析和日志排查时会非常省力。
提示:选择题遇到不确定的选项,先把它和你确定的知识点做对比,排除明显矛盾的选项,剩下的再猜。不定项选择宁少勿多,选错了整题零分,漏选至少还有机会拿一部分分(有的平台漏选给一半分)。
3. 典型编程题实操:三题拿到80%分数的写法
3.1 LRU缓存:用LinkedHashMap还是手写双向链表
LRU缓存这道题在字节笔试里出现过多次变体,2017年前后尤其频繁。题目要求:实现一个LRU缓存,支持get(key)和put(key, value),get和put的时间复杂度都是O(1)。如果大家已经熟悉Java,最快能AC的写法是利用LinkedHashMap:
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(2); cache.put(1, "a"); cache.put(2, "b"); cache.get(1); // 访问key=1,让它变成最近使用 cache.put(3, "c"); // 容量超了,淘汰最久未使用的key=2 // 输出 {1=a, 3=c} System.out.println(cache); } }这里最关键的是LinkedHashMap构造器的第三个参数accessOrder,设为true后,map会按照访问顺序排序,每次get或者put被访问的entry都会移到链表尾部。然后重写removeEldestEntry,当当前容量超过设定容量时就移除链表头部的entry,也就是最久未使用的那个。核心就这两行。
但要注意,这么写虽然AC很快,却不像一个“客户端工程师”该有的深度。我记得当时面试官后续追问了一句:你了解LinkedHashMap底层怎么维护访问顺序吗?如果之前没研究过,很容易卡壳。所以我的建议是:笔试拿分用LinkedHashMap,但笔试结束后一定要自己手写一遍双向链表+HashMap的实现。手写版本核心思路是——HashMap负责O(1)查找,双向链表负责O(1)插入和删除,每次访问节点就把它移到链表尾部。
3.2 反转链表:迭代与递归两种解法都要会
反转链表是客户端笔试里最经典的链表题,没有之一。题目非常直接:给定一个单链表头节点,返回反转后的链表。迭代写法在笔试中最稳,空间复杂度O(1):
class ListNode { int val; ListNode next; ListNode(int val) { this.val = val; } } public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode cur = head; while (cur != null) { ListNode nextTemp = cur.next; cur.next = prev; prev = cur; cur = nextTemp; } return prev; }这段代码的要点是提前保存cur.next,否则一旦把cur.next指向prev,原来的后半段链表就丢了。我当年笔试时就没写这行,结果链表直接从中间断掉,测试用例全挂。后来我养成一个习惯:凡是涉及指针修改的链表题,第一件事就是问自己“改之前要不要先存一下原始next”。
递归写法也建议掌握,它能展示你对递归的理解。思路是:先反转head之后的所有节点,然后把head接到反转后链表的尾部:
public ListNode reverseList(ListNode head) { if (head == null || head.next == null) { return head; } ListNode newHead = reverseList(head.next); head.next.next = head; head.next = null; return newHead; }递归版本的空间复杂度是O(n),因为递归调用栈会占用空间。笔试里如果链表很长,有可能爆栈,所以迭代法是默认答案。如果题目没有额外要求,优先写迭代。
注意:在线的判题系统通常会有“特殊输入”测试,比如空链表、单节点链表、两个节点的链表。写代码之前先在脑子里跑一遍这几个边界情况,能救回很多测试点。
3.3 字符串压缩:边缘情况和StringBuilder的取舍
字符串压缩也是一道很典型的“会者不难”的题。题目类似:给定一个字符串,把连续相同的字符压缩成“字符+出现次数”,例如aabcccccaaa变成a2b1c5a3。如果压缩后字符串长度不小于原字符串,则返回原字符串。
我当时写的版本是:
public String compress(String s) { if (s == null || s.length() == 0) { return s; } StringBuilder sb = new StringBuilder(); char cur = s.charAt(0); int count = 1; for (int i = 1; i < s.length(); i++) { if (s.charAt(i) == cur) { count++; } else { sb.append(cur).append(count); cur = s.charAt(i); count = 1; } } sb.append(cur).append(count); String compressed = sb.toString(); return compressed.length() < s.length() ? compressed : s; }这题有两个关键点。第一,必须用StringBuilder,而不是在循环里直接用String拼接。因为Java的String是不可变对象,每次+都会产生新对象,数据大了之后时间会爆炸,面试里也常考这个点。第二,最后别忘了处理最后一个字符的计数,很多人循环结束就以为完事了,漏掉了最后的sb.append(cur).append(count)。
扩展延伸一下,如果题目要求不区分连续,而是统计整个字符串中每个字符出现的次数,那就要换HashMap来统计,输出顺序不一定有序,需要根据题目要求决定是否排序。还有变体是要求按字符首次出现的顺序输出,那就需要LinkedHashMap。这些变体都是同一类考点:遍历字符串、统计、按需输出。
4. 笔试实战复盘:失分点、时间分配与后续准备
4.1 答题顺序与时间分配
笔试的90分钟怎么分配,直接决定你的最终成绩。我的建议是:不要按题目顺序做。收到试卷后先花2分钟把所有题目扫一遍,尤其是编程题,快速判断哪些是自己熟练的、哪些是看着就头疼的。然后按“易得分 → 必须得分 → 能拿几分算几分”的顺序做。
具体来说,先把最有把握的一道编程题AC掉,这样心里有底,后面就算时间紧也不会慌。然后做选择题,因为选择题是不定项选,考察的往往是记忆类知识,趁脑子清醒时正确率更高。简答题如果有一道Handler机制之类的,可以放在选择题之后写,因为它不需要太多思考,主要是把思路写完整。最后再啃剩下的编程题,如果卡了10分钟以上,果断跳过或者先写暴力解法保底。
很多同学喜欢先做选择题,觉得编程题思维负担大。但我的经验是:选择题里那些不定项很容易让人反复纠结,时间不知不觉就过去了。编程题如果放到最后半小时,心理压力会非常大,手一抖代码就容易写崩。先搞定编程题,心态会稳很多。
4.2 那些年我踩过的失分点
我在那次笔试里踩过的坑,以及在后来帮同学复盘时看到的常见失分点,集中整理一下:
第一,不处理空输入。编程题给你一个head,你直接while (cur.next != null),如果head本身是空的,直接空指针异常。正确做法是开头先写if (head == null)的判断。这个习惯在IDE里不明显,因为IDE会帮你补全,但在线编辑器里全靠手写,很容易漏。
第二,数组越界。尤其是涉及动态规划的题,dp[0]、dp[1]的初始化没想清楚,循环里一跑就数组越界。还有字符串的charAt(i),在循环最后一位时如果用了i+1,也会越界。
第三,忘了处理Java基本细节。比如方法返回类型是String,但最后返回的是StringBuilder;变量名拼写不一致;类名和文件名对不上。这些都算编译错误,在线编辑器可不会帮你自动修复。
第四,选择题被“不确定”拖死。不定项选择多选漏选都扣分,很多人因为一个选项不确定就反复改,最后把正确答案改没了。考场上我吃过的亏就是“再看一遍觉得A不对”,结果A是对的。后来我学乖了:除非能从原理上否定一个选项,否则不要轻易改第一感觉。
第五,时间分配失衡。编程题第一题简单,有些人写得非常卖力,追求漂亮解法,结果第二、第三题没时间看。笔试的目标不是完美,而是总分最大化,简单题能AC就不要贪。
4.3 笔试通过后,面试前应该准备什么
如果你顺利收到了面试通知,恭喜,第一关过了。但别高兴太早,笔试只是筛基本功,面试才是真正看潜力的时候。我当时在准备面试时,主要做了四件事:
一是复盘笔试题。笔试结束后趁记忆还热,把每道题重新做一遍,尤其是没做出来的题,查漏补缺。很多面试官会拿你笔试试卷当素材,直接问你“当时这道题为什么这么写”“现在还有没有更优解”。如果答不上来,会非常减分。
二是把项目经历整理成STAR结构。面试官几乎必问“你做过什么项目”。不要只说“我做了个App”,要说出背景、任务、行动、结果,最好还有具体数据,比如“优化了列表加载,卡顿率下降30%”。
三是客户端核心知识点系统过一遍。Android方向重点准备四大组件、Handler机制、RecyclerView的缓存复用、内存泄漏常见场景、OkHttp的请求流程。iOS方向重点准备OC内存管理、RunLoop、Block、KVO/KVC这些。这些都是客户端实习生面试的高频区。
四是要能白板写代码。面试里会让你现场写题,而且这次没有在线判题系统,面试官看着你写。平时练习时一定要养成“不开IDE也能写对”的习惯,多拿笔在纸上画,或者在文本编辑器里写代码不运行,写完再人工检查。这个能力练好了,面试的容错率会高很多。
我个人在实际操作中的体会是:字节这种笔试筛的不是“天才”,而是“稳定的人”。你不需要每道题都完美,但你要让阅卷的人感觉到你的思路是清晰的,代码是可信的。我当时笔试编程题只完整AC了两道,选择题还改错了好几道,但依然收到了面试通知。后来跟当时的面试官聊起来,他说笔试主要看两件事:一是遇到问题会不会拆解,二是写出来的代码能不能维护。这两个标准,后来也一直影响着我带人的方式。
如果你现在正在准备客户端方向的实习笔试,最后再分享一个小技巧:不要只刷题不看基础,也不要只背八股不写代码。把操作系统、网络这些“地基”打牢,把常见算法题练到能默写,再把代码习惯磨到不依赖IDE也能稳,这三件事做到,笔试通过率会高很多。