爱奇艺研发工程师笔试题复盘:从C++基础到算法与系统设计
爱奇艺2016研发工程师笔试题,放在今天看确实有点年份感,但如果你正准备互联网公司的研发岗笔试,这份题还是很典型的复习样本。2016年前后正是视频网站技术团队扩张最猛的阶段,爱奇艺的笔试题带着很明显的“业务与基础并重”的印记:既考C++内存模型、网络协议这些基本功,也考字符串处理、链表操作这类高频编程题,偶尔还会插入一两道跟视频业务相关的场景题。
这篇文章适合两类人看。第一类是准备大厂研发岗笔试的应届生,可以用来对照当前复习节奏,查漏补缺;第二类是工作两三年想跳槽的工程师,可以用它快速回炉基础知识点,顺便检验一下自己是不是把大学里学的东西都还给老师了。我会按题型把题目拆开讲,包括考察意图、解题思路、手写代码和实际踩坑经验。
1. 这份笔试题究竟考什么:整体定位与考察逻辑
1.1 爱奇艺笔试的行业底色与出题风格
很多人拿到笔试题的第一反应是刷题,但我觉得先搞清楚出题人想要什么,比盲目刷题更重要。2016年爱奇艺的研发笔试题,跟当时其他一线互联网公司比,有个明显特点:基础题占比很高,业务题也偏工程化,基本不会出那种偏题怪题。
为什么这样出题?一方面,视频网站的业务场景对稳定性要求极高,用户看视频看到一半崩溃,体验损失比普通网页大得多,所以招聘时特别看重候选人有没有扎实的底层功底。另一方面,2016年视频行业正处于移动端爆发期,高并发访问、播放体验优化、个性化推荐这些都是当时的技术重点,笔试题自然也会往这些方向靠。
这告诉我们的备考逻辑其实很简单:核心技术基础够不够硬,决定了笔试能不能过;有没有工程思维和业务敏感度,决定了面试官愿不愿意给你下一轮机会。
1.2 笔试考察的四个能力维度
爱奇艺这套题看似零散,实际上可以归纳成四个维度。
第一个是语言基础,C/C++是重点,特别是内存管理、指针、虚函数、构造析构这些,因为客户端播放器和部分服务端模块都用C++,这些知识点直接关系到底层稳定性。第二个是数据结构与算法,字符串反转、链表操作、二叉树遍历、排序算法都属于必考范围,笔试环节算法题占比大约三分之一,手写代码的基本功在这里会被看得一清二楚。
第三个是操作系统与网络,进程线程区别、死锁条件、TCP三次握手、HTTP状态码这些经典问题,几乎每次笔试都会遇到。第四个是业务场景题,这类题在2016年爱奇艺的笔试里已经出现了,比如视频播放卡顿怎么排查、热门视频如何设计缓存,都属于“给你一个真实问题,看你怎么拆解”的路子。
1.3 这份题适合谁来参考
如果你正在准备校招笔试,这套题可以作为中期复习的测评卷。我的建议是,先不要看答案,给自己定一个半小时的计时,完整做一遍,感受一下时间压力,再对照解析看自己卡在哪类题目上。
如果你已经工作了一段时间,这套题的价值在于查漏补缺。我见过不少工作两年以上的工程师,写业务代码很溜,但让他说清楚“进程和线程的本质区别”或者“为什么TCP要三次握手”,反而讲不完整,这类基础漏洞在跳槽笔试时很容易暴露。
2. 真题复盘:基础理论与概念题拆解
2.1 常考题型分布速览
先看整体分布。根据当时多家培训机构整理流传的笔经,爱奇艺2016研发工程师笔试题大致可以归为以下几类。
| 题型 | 数量占比 | 考察重点 |
|---|---|---|
| 单选题 | 30%左右 | 语言基础、操作系统、网络常识 |
| 不定项选择 | 15%左右 | 边界条件、概念辨析、代码输出结果 |
| 编程题 | 35%左右 | 数据结构、算法设计与手写实现 |
| 简答/场景题 | 20%左右 | 视频业务、系统设计、问题排查思路 |
这个分布说明,选择题和编程题几乎是半壁江山,纯背诵型知识点占比不高,更多是理解型和应用型考察。
2.2 典型单选题:C++虚函数与内存布局
有一道很经典的单选题,问的是“含有虚函数的类,实例化后的对象内存中第一个成员是什么”。答案是虚函数表指针,也就是vptr,通常占4字节(32位系统)或8字节(64位系统),位于对象内存布局的最前面。
这道题表面考虚函数,实际考的是C++对象模型的理解。很多刷面经的人背过“虚函数表”这几个字,但不清楚为什么对象里要存一个指向虚函数表的指针。简单解释一下:C++支持多态,靠的是运行时动态绑定,当通过基类指针调用一个virtual函数时,编译器并不知道实际对象是哪个子类,只能在运行时去查虚函数表,才能确定应该调用哪个版本的函数。这个查询动作就是“动态绑定”,而查询的入口,就是对象内存里那个隐藏的vptr。
我当年复习的时候,喜欢自己画一下内存布局图,把基类和子类的vptr、成员变量排列画出来,比背十遍概念都管用。
2.3 典型选择题:数组与指针的关系陷阱
还有一道高频题,大概长这样:给定int a[5],请问(&a + 1)表示什么?如果你直接想成“数组首地址加1”,那这道题就错了。&a是整个数组的地址,类型是int(*)[5],所以&a + 1跳过的不是一个int,而是整整5个int,也就是跳过了整个数组。
这里面的核心陷阱,是区分“数组首元素的地址”和“数组的地址”这两个概念。a在大多数表达式中会退化为指向首元素的指针,但&a始终是指向整个数组的指针,两者的步长完全不同。这类题在笔试里出现频率极高,不完全是因为爱奇艺爱考,而是因为它是考察C语言底层理解的一个经典切片,很多工作了几年的人都容易答错,我把它当作“基本功试金石”来看。
2.4 不定项选择题:进程线程与死锁
不定项选择的典型代表是“下列关于进程和线程的说法,哪些是正确的”。正确选项一般包括:进程是资源分配的基本单位,线程是CPU调度的基本单位,同一进程内的线程共享地址空间,不同进程的地址空间相互隔离。容易选错的干扰选项是“线程切换一定比进程切换快”和“线程可以完全替代进程”。
为什么“线程切换一定比进程切换快”不对?因为线程切换虽然不用切换地址空间,但还是要保存和恢复寄存器上下文、程序计数器等,如果两个线程不在同一个CPU核心上,还可能涉及缓存失效。而且进程切换的代价里,很大一部分是页表切换和TLB刷新,这些在线程切换中确实可以避免,但绝对不能推出“一定比进程快”。这类题目用的就是绝对化表述作为陷阱。
同样的逻辑也出现在死锁题里。考死锁条件的时候,记得是四个必要条件:互斥、占有且等待、不可抢占、循环等待。题目如果问“破坏哪个条件可以有效防止死锁”,答案通常是从“占有且等待”或“循环等待”入手,比如资源一次性分配、按序分配等。
2.5 网络与系统:三次握手和HTTP状态码
网络题里,TCP三次握手几乎是必考。选择题一般考握手的顺序和标志位,比如SYN、SYN+ACK、ACK,简答题则可能让你说明“为什么需要三次握手而不是两次”。
这个问题的标准解释是:三次握手能确保双方都确认自己和对方的收发能力正常。第一次客户端发SYN,服务器知道客户端发能力正常;第二次服务器回SYN+ACK,客户端知道服务器收能力正常、发能力也正常;第三次客户端回ACK,服务器知道客户端收能力正常。如果只有两次握手,服务器无法确认客户端的接收能力,可能导致已经失效的连接请求突然到达服务器,白白建立一条空连接。
HTTP状态码也是高频考点。我建议至少记住:200正常、301永久重定向、302临时重定向、304未修改(缓存相关)、400请求错误、401未认证、403禁止访问、404不存在、500服务器内部错误、502网关错误、503服务不可用。爱奇艺这种视频站点,用户会频繁触发缓存和重定向逻辑,所以304、301、302这几个状态码在业务场景里会特别常见。
3. 核心算法题的思路与手写实现
3.1 字符串类题目的考场解法
字符串处理是当年爱奇艺笔试编程题的大头,常见的有字符串反转、括号匹配、最长公共前缀、字符串去重等。难度不高,但非常考验边界条件处理能力。
举个例子:反转字符串中的单词顺序,要求单词内部字符顺序不变,比如输入"the sky is blue",输出"blue is sky the"。很多人第一反应是先按空格切分,再逆序遍历拼接,但要注意连续多个空格的情况。LeetCode原题的官方解法是先把整个字符串反转,再逐个反转每个单词,我在笔试里也推荐这种做法,因为原地处理,空间复杂度是O(1)。
def reverse_words(s: str) -> str: s = s.strip() words = [] i = 0 n = len(s) while i < n: while i < n and s[i] == ' ': i += 1 start = i while i < n and s[i] != ' ': i += 1 if start < i: words.append(s[start:i]) return ' '.join(words[::-1])笔试改卷时最看重的是边界条件,比如字符串为空、全是空格、只有一个单词、首尾都有空格。我见过不少同学能写出核心逻辑,但因为没处理首尾空格被扣分,非常可惜。
3.2 链表题:反转与环检测
链表在笔试编程题里的地位极高,原因很简单:实现链表的代码不长,但能考察指针操作、递归思维和边界条件处理,信息密度很高。
两道必练题是“反转链表”和“环形链表检测”。反转链表用迭代法最稳,三指针prev、curr、next依次反转,注意循环结束后要把原来的头结点的next置空,否则会产生环。
struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr != nullptr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; }环形链表检测最经典的是快慢指针法,fast指针每次走两步,slow指针每次走一步,如果有环,两者必然相遇。我当时踩过的坑是忘记处理空链表和单节点链表的情况,fast->next空指针访问会直接崩溃,笔试环境不像本地IDE那么好调试,所以开始写之前一定要先把特殊情况列出来。
3.3 二叉树遍历与层级输出
二叉树题目在2016年爱奇艺笔试中也出现过。最基础的是前中后序遍历的递归实现,但笔试往往不会只考递归,更多会考非递归和层序遍历。
层序遍历有一个常见变形:要求按层级分组输出,比如第一层一个列表,第二层一个列表。很多人的第一反应是直接队列遍历,但这样分不清层级边界。正确做法是每次循环先记录当前队列的长度,然后只处理这个长度个节点,这样就天然分好了层级。
vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> result; if (!root) return result; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int levelSize = q.size(); vector<int> level; for (int i = 0; i < levelSize; ++i) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } result.push_back(level); } return result; }笔试中这类题目的考察重点不是你会不会递归,而是你会不会在递归之外换一种思路。平时练题的时候我建议有意识地把每道递归题都写一遍非递归版本,练的是栈和队列的灵活运用,这个习惯对面试手撕代码也很有帮助。
3.4 动态规划:最长递增子序列
动态规划在笔试里属于区分度高的题型,爱奇艺这类视频公司还涉及推荐、个性化排序等场景,动态规划出现频率不低。最长递增子序列(LIS)就是典型题目。
最经典的写法是O(n^2)的DP:dp[i]表示以nums[i]结尾的最长递增子序列长度,转移时遍历i之前的所有j,如果nums[j] < nums[i],状态转移方程为dp[i] = max(dp[i], dp[j] + 1)。
def length_of_lis(nums): if not nums: return 0 n = len(nums) dp = [1] * n for i in range(n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp)O(n^2)写法思路直白,适合笔试,因为代码不容易出错。如果你追求更高性能,可以用贪心加二分,维护一个tails数组,把时间复杂度降到O(n log n),但实现难度会大一些。我的建议是笔试时先写最稳的版本,把正确率保住,时间允许再优化。万一面试官要求优化,再展示你在O(n^2)基础上的进阶思路,效果反而更好。
4. 场景题与业务题:视频网站工程思维的模拟
4.1 从用户点击到视频播放:排查题怎么答
爱奇艺的笔试里,场景题通常不像算法题那样有标准答案,但回答得好不好,一眼就能看出你是有工程经验,还是只会背题。
一道很典型的场景题是:“用户在App里点开一个视频,播放卡顿,请描述你的排查思路。”如果你只回答“可能是网络不好”,这道题基本就废了。好的回答要有层次感。
我的回答框架是先分段再排查。第一步确认是偶发还是必现,如果必现,问题大概率在服务端或视频源;如果偶发,优先怀疑网络。第二步检查客户端到CDN节点的网络质量,包括丢包率、RTT、DNS解析耗时,可能的话用抓包工具看有没有大量TCP重传。第三步看播放器状态,是首屏加载慢,还是播放中频繁卡顿,两者对应的问题可能完全不同,前者可能跟首包耗时有关,后者可能跟缓冲策略、码率切换有关。第四步看服务端,检查视频转码格式、切片大小、CDN命中率、源站负载。
这些排查点在笔试里不需要写得很深,但要让阅卷人看到你有完整的排查链路,而不是零散地猜原因。
4.2 热门视频榜单:缓存与排序设计
另一类场景题是“如何设计一个热门视频排行榜”。这类题在视频公司出现完全合理,而且没有唯一答案,考察的是你能否结合业务场景做合理取舍。
核心思路是“写入时计数、读取时聚合”。用户观看行为会产生大量的播放日志,如果每次播放都直接更新数据库的计数器,数据库压力会很大。更稳妥的方案是先把播放事件写入消息队列,后台异步消费,定期聚合到Redis里,排行榜直接从Redis读取。
具体到排序维度,常见的有播放量、完播率、点赞数、分享数,也可以做加权综合分,比如score = 播放量 * 0.5 + 完播率 * 0.3 + 互动量 * 0.2。这里要注意“时间的衰减”,一个三天前爆火的视频和一个三小时前爆火的视频,即使播放量相同,热度也不一样,可以用类似Hacker News的算法,用发布时间对分数做降权。
笔试里答这类题,关键是展示你有“分层”的思维:接入层、处理层、存储层各司其职,而不是把一大堆功能堆在一个模块里。哪怕是简单的文字描述,也要体现出“我知道哪里是瓶颈、哪里要加缓存、哪里要异步化”的工程判断。
4.3 日志收集与布隆过滤器
视频网站的访问日志量非常大,笔试偶尔会考一个衍生问题:“如何判断一个URL是否已经在今天的日志中出现过?”如果直接存HashSet,每个URL几字节,上亿条数据就是几个GB内存,服务端显然扛不住,这时候可以用布隆过滤器。
布隆过滤器的原理很简单:一个位数组加多个哈希函数,插入元素时把多个哈希位置置1,判断元素是否存在时检查这些位置是否都为1。如果有任何一个位置为0,元素一定不存在;如果全部为1,元素可能存在,也就是有误判率。误判率可以通过位数组大小和哈希函数数量来调节。
优缺点也明显。优点是空间效率极高,缺点是没法删除元素,且存在误判。在笔试里讲清楚“为什么用布隆过滤器”以及“误判率如何权衡”,比背出实现代码更重要。我当时复习时总结过一句话:布隆过滤器适合“允许小概率误判但绝不允许漏判”的场景,比如URL去重、黑名单过滤都符合这个特征。
5. 备考路径与常见问题排查
5.1 复习优先级建议
结合爱奇艺这套题和同类企业笔试题的出题特点,我建议准备研发岗笔试的同学按照下面这个顺序安排复习。
第一优先级是数据结构与算法,包括数组、字符串、链表、栈、队列、二叉树、哈希表,以及排序、二分查找、双指针、动态规划这些核心算法。笔试的绝对大头在这里,编程题能不能写出来,直接决定你能不能进入面试环节。第二优先级是语言基础,如果你主攻C++,虚函数、内存管理、STL底层原理、智能指针都要过一遍;如果你主攻Java,JVM内存模型、集合框架、并发编程要重点关注。
第三优先级是操作系统和网络的核心概念,不需要抠太细,但进程线程、死锁、内存管理、TCP/UDP、HTTP这些高频考点要能讲清楚。第四优先级是场景题和业务常识,这部分可以结合目标公司的业务特点来准备,比如投视频公司,就想想视频播放链路、CDN、排行榜这些场景。
5.2 考场时间分配与做题节奏
笔试时间通常很紧张,我见过太多人因为时间分配不当,编程题没写完,或者最后几道选择题草草蒙完。我的建议是拿到卷子先花两分钟扫一遍全卷,搞清楚大题的题量和难度,心里有个底。
具体节奏上,选择题控制在每道题一分半以内,不会的先标记跳过,不要恋战。编程题每道至少预留20到30分钟,如果15分钟内一点思路都没有,果断换下一道,最后再回来啃硬骨头。场景题一般留10到15分钟,不需要写太多字,但要把框架写清楚。
这里提醒一个关键习惯:写代码前先在草稿纸上梳理思路和边界条件,不要直接上手敲。很多人一紧张就直接写,写到一半发现思路错了,改来改去浪费大量时间,反而得不偿失。
5.3 高频失误与针对性改进
我总结一下笔试题里最容易丢分的几个点,算是给后来人排雷。
第一,边界条件处理不完整。空数组、单元素数组、全是重复元素、链表只有一个节点、根节点为空,这些特殊情况必须在写完代码后主动检查。第二,复杂度分析写不清楚。编程题如果要求说明时间复杂度和空间复杂度,常常有同学写错,比如把二分查找写成O(n),明显是概念没掌握。第三,代码可读性差。变量名用a、b、c不是不行,但关键逻辑处至少要有注释,笔试阅卷很多环节是人工看的,代码整洁度会影响印象分。
我当年还吃过一个亏,就是写完代码不检查数组越界。笔试环境不像本地IDE有清晰的运行时提示,有时候数组越界不会立刻崩溃,而是产生一个奇怪的结果,这种错误最难查。所以每次写完循环,我都会手动跑一个小例子,把循环变量的变化过程走一遍,确认不会越界再提交。
5.4 常见问题速查表
为了让你复习时方便对照,我把这套题暴露出的高频问题整理成一个速查表。
| 问题 | 错误理解 | 正确理解 |
|---|---|---|
| 虚函数机制 | 虚函数表存在对象里 | 对象里存虚函数表指针,虚函数表一般存在于只读数据段 |
| 数组名退化 | &a和a完全等价 | &a指向整个数组,a通常指向首元素 |
| 线程切换 | 线程切换一定比进程切换快 | 不一定,要分场景,但通常线程切换开销更小 |
| TCP握手次数 | 两次握手就够了 | 三次才能确保双方收发能力都确认 |
| 死锁条件 | 有循环等待就一定死锁 | 死锁必须同时满足四个必要条件 |
| 进程同步 | 线程之间无法同步 | 可以通过信号量、锁等机制同步,但要注意死锁 |
| 动态规划 | 只求dp数组最大值 | 要明确dp数组的含义和转移方程 |
| CDN作用 | 加速所有网络请求 | 主要加速静态资源分发,动态请求回源成本高 |
6. 一点个人经验:为什么现在还要看这份旧题
聊到这儿,可能有人会问:2016年的题,现在还有什么参考价值?我的看法是,研发工程师笔试的底层层面的知识点迭代速度没那么快。很多算法题和基础概念题,在今天的笔试里依然是变形出现,比如反转链表、字符串处理、二叉树遍历,换个措辞换个包装,内核不变。
我看这类旧题的主要价值有三个。第一个是摸底,找一套完整的旧题限时做一遍,比盲目刷几十道零散题目更能准确知道自己的水平。第二个是练题感,旧题往往没有过度加难度,用来建立信心和熟悉笔试节奏很合适。第三个是理解“公司想要什么人”,出题风格能反映公司技术文化的倾向,爱奇艺这套题呈现的务实风格,在那个年代算很有代表性的。
我个人的体会是,复习笔试最重要的不是题海战术,而是把每道题背后的知识点彻底吃透。一道反转链表,你能讲出迭代和递归两种写法,能分析空间复杂度差异,能处理带头结点和不带头结点的变体,那这道题才算真正会了。做十道题却一知半解,远不如做一道题把一个知识点完全啃透,后者在笔试里的实际效果要好得多。
最后再分享一个小技巧:每次做完一套笔试题,别急着对完答案就完事,花半小时在草稿纸上列一个“我错了什么知识板块”的清单,然后针对性地找同类型题目集中刷几道。这套方法我从校招一直用到跳槽,每次都很管用。你现在拿这套题目练手,按这个流程来,笔试能力应该会有很明显的提升。
