2016年那会儿的视频行业正是百舸争流的时候,爱奇艺的研发工程师笔试题在圈内以“范围广、基础深、偏实战”著称。我当年刷过这套题,也帮不少人复盘过,很多题目哪怕放到今天依然有很强的参考价值——尤其是考察你对算法边界条件的敏感度、对系统设计的取舍能力,这些恰恰是日常业务开发中最容易翻车的地方。
这篇东西我不打算逐题报答案,那样没什么意义。我更想从这套题里提炼出几个经典问题作为代表,把解题思路、代码实现、边界条件、以及题目背后的考察意图掰开揉碎讲清楚。不管你是准备面试,还是想查漏补缺,都能从中找到点东西。
1. 这套题的主线:不考偏题,专考“基础是否扎实”
爱奇艺2016年的研发笔试题整体风格很鲜明:不搞脑筋急转弯,不追求冷门偏题,而是把大量精力放在计算机基础知识的深度理解上。整套题大致分为算法与数据结构、操作系统、网络、数据库、逻辑推理几个模块,其中算法与数据结构的比重最高,大约占40%以上。
这里有个挺有意思的信号:视频网站的后端服务面临的核心挑战是超高并发下的资源调度、缓存策略、流媒体传输优化,所以笔试中反复出现数组操作、字符串处理、链表反转这类题目,并不是因为出题人偷懒,而是这些基础数据结构恰恰是构建高性能服务的底层积木。比如数组去重背后是Hash表的应用思想,链表反转背后是指针操作的熟练度,二分查找背后是边界条件的把控能力——这些都是在真实业务中写代码时会直接影响Bug率的硬功夫。
我当时做完这套题的最大感受是:题目本身不难,难的是在有限时间内把每个细节都处理对。很多题你一看就会,一写就错,错就错在边界条件、空指针、溢出这些“小地方”。这恰恰是笔试筛选人的核心逻辑——基础扎实的人,在这些细节上几乎不需要犹豫。
2. 高频算法题拆解:从需求到代码的完整推演
2.1 数组去重与排序:考察你写代码是否“干净”
这套题中有一道非常典型的题目:给定一个无序数组,要求去除重复元素并按照从小到大的顺序输出。
这道题看似简单,但考察点其实很丰富。首先你需要明确数组是否有序——如果先排序再去重,时间复杂度取决于排序算法;如果借助HashSet去重再排序,则时间复杂度稳定在O(nlogn)。面试官真正想看的不是你能不能写出来,而是你能否分析不同方案的时间和空间复杂度,以及能否处理数据范围超出常规限制的情况。
我当时给的答案是用HashSet加Collections.sort,代码非常短:
public List<Integer> removeDuplicatesAndSort(int[] arr) { Set<Integer> set = new HashSet<>(); for (int val : arr) { set.add(val); } List<Integer> result = new ArrayList<>(set); Collections.sort(result); return result; }但考官的追问来了:如果数组长度是几千万,内存装不下怎么办?这时候就要想到外部排序的思想——借助多路归并或者MapReduce框架处理。笔试虽然不用你真正实现外部排序,但你必须具备这个意识,要能讲清楚“单机内存不够时的处理思路”。这道题的核心考点其实是:你写代码时有没有考虑数据规模对方案选型的影响。
2.2 链表反转的三种写法:迭代、递归、头插法
链表反转是笔试中的常青树,爱奇艺的这套题里也出现了。很多人背了迭代版本的代码就问心无愧了,但实际上这道题至少有三种写法,理解深度完全不一样。
迭代版本最直接,用三个指针prev、current、next依次翻转:
public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode current = head; while (current != null) { ListNode nextTemp = current.next; current.next = prev; prev = current; current = nextTemp; } return prev; }这里有两个关键点:一是必须先用nextTemp保存当前节点的下一个节点,否则一旦修改了current.next就丢失了后续链表的引用;二是循环结束后prev指向的是新的头节点,这也是要返回的节点。
递归版本则更考验对递归思想的理解:
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; }递归版本的核心逻辑是:假设head.next之后的子链表已经完成反转,那么只需要让原head.next节点的next指向head即可。这段代码写起来简洁,但理解起来需要一定功力。我建议读者在纸上画一下递归调用栈的展开过程,把这个过程吃透,以后碰到更复杂的链表问题会轻松很多。
2.3 字符串中第一个只出现一次的字符
这道题考察的是对Hash表以及字符编码的理解。要求很简单:给定一个字符串,找到第一个只出现一次的字符并返回其下标。
最直观的做法是两次遍历:第一次遍历统计每个字符出现的次数,第二次遍历查找第一个次数为1的字符。
public int firstUniqChar(String s) { int[] freq = new int[26]; for (char c : s.toCharArray()) { freq[c - 'a']++; } for (int i = 0; i < s.length(); i++) { if (freq[s.charAt(i) - 'a'] == 1) { return i; } } return -1; }这里有一个容易被忽略的细节:如果字符串包含的不只是小写字母,而是Unicode字符或者中文,int[26]就不够用了,需要改用HashMap。笔试中要看清题目的字符范围假设,如果没说小写字母,稳妥的做法是直接使用HashMap,避免踩进“隐含条件”的坑。
注意:凡是涉及字符统计的题目,先问清楚字符集范围。小写字母26个、ASCII 128个、还是Unicode全量?不同范围直接决定用数组还是用HashMap。这个细节看起来小,但在真实业务里,字符集判断错了就是线上事故。
3. 智力推理题的破题思路:看似玄学,实则有规律
爱奇艺这套题里有一小部分智力推理题,比如经典的“找假币”“过桥问题”“房间开灯问题”等。这类题目考察的不是死记硬背,而是逻辑建模能力——把一个看似无序的情况抽象成可以用数学工具描述的模型。
拿找假币问题举例:有n枚硬币,其中一枚较轻,用天平最少称几次能找出假币?这个问题的本质是利用天平的三种结果(左重、右重、平衡)来构造三分搜索,每次称量可以获得三分之一的缩小比例。所以n枚硬币所需的次数是log3(n)向上取整。
我当时做这类题目有个心得:不要凭空想,先在纸上列出几种可能性,然后尝试归纳规律。很多智力题的本质都是信息编码问题——每次操作能获得多少比特的信息量,决定了最优次数。这个视角一旦建立,这类题目就不再是玄学,而是有章可循的技术问题。
再比如经典的“100层楼扔鸡蛋”问题,看起来是脑筋急转弯,实际上是动态规划的最优策略求解。状态转移方程是:
dp[i][j] = 1 + min(max(dp[k-1][j-1], dp[i-k][j])),其中k从1到i
这里dp[i][j]表示i层楼j个鸡蛋在最坏情况下所需的最少尝试次数。这个方程的含义是:第一次从k层扔,如果碎了,就向下搜索k-1层(此时鸡蛋数减一);如果没碎,就向上搜索i-k层(鸡蛋数保持不变)。我们要选择最优的k,使最坏情况下的尝试次数最少。
说实话,这类动态规划题在笔试中出现频率不算高,但一旦出现,分值不低。如果你在短时间内推导不出来,我的建议是写出暴力递归版本,然后说明“可以通过记忆化搜索优化到O(n²·m)”,这样至少能拿到部分分数。
4. 操作系统的“暗礁”:死锁、线程同步与内存管理
操作系统部分的题目看着基础,实际上是整套卷子里区分度最高的部分。爱奇艺的题目在操作系统上问得很细,考的不是“死锁产生的原因”这种背诵题,而是给你一段代码或一个场景,让你判断是否可能发生死锁。
例如,经典的哲学家就餐问题:5个哲学家围坐在圆桌旁,每个人需要两只筷子才能进餐。如果每个人都先拿起左边的筷子再拿起右边的筷子,就可能发生死锁——所有人都拿着一只筷子等另一只筷子。解决方案有很多:给筷子编号,规定必须先取编号小的筷子;或者限制最多只有4个人同时拿起筷子;或者使用信号量控制临界区。
考察这段代码的关键是理解死锁的必要条件:互斥、占有并等待、不可剥夺、循环等待。这四个条件缺一不可。面试和笔试中遇到死锁题,不要去猜“会不会死锁”,而是逐一检查这四个条件是否满足,答案自然就出来了。
线程同步的考察重点集中在synchronized与Lock的区别上。synchronized是JVM层面的锁,使用方便但功能有限;Lock是JDK提供的接口,支持公平锁、非公平锁、可中断锁、超时获取锁等更精细的控制。2016年那会儿很多候选人对Lock的理解还停留在“知道用法”层面,很少能讲清楚在ReentrantLock中,FairSync与NonfairSync的实现差异——非公平锁在获取锁时会先做一次CAS尝试,如果成功就直接获取,不进入等待队列;公平锁则严格按照先来后到的顺序。这个差异在高并发场景下直接影响系统吞吐量。
内存管理部分考察了堆和栈的区别。很多人回答“堆存对象,栈存引用”就交卷了,但实际上笔试的考察意图是让你理解两者的生命周期和线程共享性:栈是线程私有的,存储局部变量、方法调用帧,方法结束即释放;堆是所有线程共享的,存储对象实例,由GC统一管理。在JVM调优时,调整堆大小和栈大小的参数不同,影响范围也不同,这些才是生产环境中真正会用到的知识。
5. 网络基础:从TCP三次握手到HTTP协议细节
网络部分的题目爱奇艺考得也比较扎实。TCP三次握手的题目大家都会背,但换个角度问你“为什么需要三次握手”很多人就答不上来了。
三次握手的核心目的是确认双方的收发能力都正常。第一次握手,客户端发送SYN,客户端确认自己发送能力正常、服务端接收能力正常;第二次握手,服务端发送SYN+ACK,服务端确认自己发送能力正常、客户端接收能力正常,同时确认客户端发送能力正常;第三次握手,客户端发送ACK,服务端确认客户端接收能力正常。如果只有两次握手,服务端无法确认客户端是否收到了自己的SYN+ACK,也就无法确认客户端的接收能力,这会带来已失效连接请求的问题——某个迟到的SYN包可能导致服务端建立无效连接。
HTTP相关题目也很常见,尤其是GET和POST的区别。2016年那会很多人照本宣科背“GET是幂等的、POST不是”。这种说法不够严谨。严格来说,HTTP方法本身不规定幂等性,而是语义上建议GET、PUT、DELETE具有幂等性。如果服务端实现不当,GET请求完全可以修改数据——虽然这不符合规范,但技术上无法阻止。更准确的表述是:HTTP设计上建议GET用于查询、POST用于提交,浏览器和网关对两者的处理策略不同,比如GET请求可被缓存、POST不可缓存,GET请求的URL长度受浏览器限制,POST请求的body大小由服务器配置决定。
6. 数据库设计题:从ER图到SQL优化
爱奇艺笔试中数据库部分也占了一席之地,核心考察方向是SQL编写能力、事务隔离等级、以及索引原理。
有一道典型的SQL题是:有一个员工表,字段包括id、name、department_id、salary,要求查出每个部门工资最高的员工信息。很多人的第一反应是使用GROUP BY加MAX函数:
SELECT department_id, MAX(salary) FROM employee GROUP BY department_id;这个写法没问题,但只能查出部门ID和最高工资,查不出对应的员工信息。要查完整员工信息需要用到联表查询或者窗口函数:
SELECT e.* FROM employee e INNER JOIN ( SELECT department_id, MAX(salary) AS max_salary FROM employee GROUP BY department_id ) t ON e.department_id = t.department_id AND e.salary = t.max_salary;这个解法考察的是对“分组后取最大值所在行”的理解。如果题目允许使用窗口函数(MySQL 8.0+),也可以用ROW_NUMBER():
SELECT id, name, department_id, salary FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 1;索引部分的题目考的是最左前缀原则。联合索引(a, b, c)实际上可以用于a、a+b、a+b+c三种条件的查询优化,但不能用于单独的b、单独的c、或者b+c组合。这个原则在业务开发中经常被忽视,建了一堆冗余索引导致写入变慢,笔试考这个其实是考察你有没有实际调优经验。
事务隔离等级这块,爱奇艺问的是不同隔离级别下幻读的解决方案。可重复读InnoDB默认的隔离级别,通过MVCC实现快照读,但普通的select是快照读,不会加锁;如果要防止幻读,需要用到当前读,即使用SELECT...FOR UPDATE或LOCK IN SHARE MODE,配合间隙锁锁定查询范围,才能阻止其他事务插入新记录。这个理解在生产环境中非常重要,尤其在处理秒杀场景、订单状态流转时。
7. Java与C++基础题:语言特性背后的设计逻辑
笔试中Java和C++的题目也不少。爱奇艺那套题里有一道关于Java重载(Overload)与重写(Override)的题目,不是简单地让你说出区别,而是给出几组代码让你判断哪一组能编译通过。
重载发生在同一个类中,方法名相同、参数列表不同,对返回类型没有要求——但只有返回类型不同且参数列表相同的两个方法无法共存,因为JVM无法通过返回类型区分方法。重写发生在父子类之间,要求方法名、参数列表、返回类型都一致(或返回类型是父类方法返回类型的子类),访问修饰符不能比父类更严格,抛出的异常不能比父类更广。
还有一个常见考点是Java中String、StringBuilder、StringBuffer的区别。String是不可变的,每次修改都会创建新对象,适合字符串不经常变化的场景;StringBuffer是线程安全的,方法加了synchronized,适合多线程环境下的字符串拼接;StringBuilder是线程不安全的,但性能最好,单线程环境下推荐使用。很多人在笔试中能写对三者的区别,但遇到“为什么String要设计成不可变”这样的追问就卡壳了。不可变的核心原因有三个:缓存Hash值提高HashMap查找效率、保证String对象在线程间安全共享、支持字符串常量池复用。
C++部分,爱奇艺考了虚函数和虚函数表的实现机制。虚函数表是每个包含虚函数的类在编译期间生成的一张函数指针表,每个对象通过虚函数指针指向它所属类的虚函数表。当通过基类指针或引用调用虚函数时,程序运行时根据对象实际的类型在虚函数表中查找对应函数地址,实现动态绑定。这也就是多态的底层原理。理解了这点,就能理解“为什么虚函数不能是静态的”“为什么构造函数不能是虚函数”——静态函数没有this指针,无法访问虚函数表;构造函数执行时虚函数表还未完全初始化,无法完成动态绑定。
8. 备考策略:怎样高效吃透一套笔试题
基于我刷爱奇艺2016年这套题的经验,给大家分享几个备考策略,比单纯刷题更有效。
第一,做题时严格控制时间。笔试的时间是很紧的,平均每道算法题给的时间大概15-20分钟。我建议你准备一个计时器,严格按照考试节奏来做,这种方式能让你提前适应真正的考试状态。平时不限时地慢慢琢磨,和限时实战完全是两种体验。
第二,每道题做完之后一定要总结复杂度。时间复杂度和空间复杂度是笔试中的必考项,如果你只写代码不分析复杂度,考试时会吃大亏。我习惯用表格记录每道题的关键信息,包括解题思路、复杂度、易错点,方便考前快速回顾。
第三,重点关注边界条件。我刷题时最大的教训就是:很多题不是不会做,而是不是边界条件没考虑周全。空数组、单元素数组、全重复数组、字符集越界、整数溢出,这些都是笔试题里最常见也最隐蔽的坑。建议把常见的边界条件列成一个checklist,每次写完代码后逐项检查,能显著提高代码的通过率。
第四,不要忽视数学基础。爱奇艺这套题里的智力推理题和管理题多少都涉及数学建模能力。考试前把基础的排列组合、概率论知识过一遍,对这类题目会有很大帮助。
第五,学会写伪代码。真正笔试时你可能被要求不给IDE环境,手写代码在纸上或白板上。如果你平时依赖IDE的自动补全,建议备考期间多加练习手写代码。遇到思路不太确定、完整的代码写不出来的情况,写伪代码也比留白强——阅卷人至少能看到你的思路,能给你部分分数。
9. 从这套题反推:爱奇艺招聘看重什么
我复盘了整套题之后,对爱奇艺研发工程师的素质要求有了更清晰的认识。他们真正在筛选的是三件事:
第一是“基础是否牢固”。整套卷子的题目几乎看不到花哨的炫技题,全部是计算机基础知识的灵活运用。这意味着如果你把数据结构、操作系统、网络、数据库这几门核心课程吃透了,笔试不会难倒你。
第二是“分析问题是否成体系”。很多题目都带有场景背景,比如给出一个并发场景问你如何设计缓存方案,或者给出一个SQL性能问题问你如何优化。这些题目没有标准答案,考察的是你分析问题的思路是否清晰、是否考虑全面。回答这类问题时,我建议遵循“先说方案总体思路,再细化关键技术点,最后谈异常情况和取舍”的框架,这个回答结构本身就能展示你的逻辑能力。
第三是“在时间压力下能否保持代码质量”。笔试的时间限制逼着你必须在短时间内写出正确的代码。这不仅是技术问题,更是心理素质和职业素养的体现。能在压力下保持冷静、有条不紊地分析问题的人,往往也是团队中值得信赖的工程师。
我有个朋友当年参加了爱奇艺的笔试和面试,最终拿到了offer。他复盘时说了一句话给我印象很深:“这套题不欺负人,每一道题都能看出出题人希望你掌握的技能树。你平时学得踏不踏实,一考就知道。”这句话到今天依然适用。希望这篇拆解能帮到正在准备笔试的你。如果你们在具体的题目上有讨论的,欢迎在评论区继续聊,我看到了都会回。