十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

爱奇艺2016研发笔试题(二)复盘:核心考点与答题策略

爱奇艺2016研发笔试题(二)复盘:核心考点与答题策略 聊到爱奇艺2016研发工程师笔试题二这套卷子很多当年一起准备校招的朋友应该都不陌生。那会儿在线视频正处在快速增长的阶段爱奇艺研发岗的笔试分成多套卷子二这套整体上走的是“基础扎实工程敏感”的路子和纯互联网公司的通用笔试题相比多了一点音视频、CDN调度和用户侧场景的味道。如果你正准备视频类公司的研发岗或者想在笔试阶段检验一下自己的基本功这套题的复盘价值确实不小。我当年刷这套题的时候很多东西也是模棱两可后来工作了几年、自己也开始参与出题再回头看才发现这套题表面上考的是知识点实际上考的是你有没有用工程思维去理解技术。这篇文章我分几个维度把整套卷子的考点、出题逻辑和答题策略拆开讲清楚每个部分都配了可以直接拿去练的示例和代码希望能帮你少走点弯路。1. 先捋清楚这套题到底在考什么1.1 一张卷子里的能力画像爱奇艺2016研发工程师笔试题二不是一套只考算法的卷子。这类笔试题通常被设计成“能力画像”式的筛选工具通过一道题判断你多个维度的情况。我把整套卷子的考察面整理成了下面这张表你可以对照着看看自己哪块比较薄弱考察维度代表性考点出题目的编程语言基础C虚函数、内存对齐、关键字作用确认你写代码不是只会背语法而是理解底层机制数据结构与算法链表反转、滑动窗口、字符串处理考察逻辑思维、边界控制、时间空间复杂度意识操作系统进程与线程、死锁条件、内存管理判断你对系统资源的理解是否到位计算机网络TCP三次握手、HTTP状态码、DNS衡量你排查线上问题的基础能力简单系统设计缓存淘汰、限流、并发处理考察工程师有没有从业务场景出发的全局视野这套题和纯算法题最大的区别在于它要求你既能把代码写对又能解释清楚“为什么这么写”。很多人在笔试环节只关注代码能不能跑通忽略了对原理的阐述这在爱奇艺这类重视工程质量的公司笔试里是很容易丢分的。1.2 视频公司的笔试题为什么“不一样”同样是研发笔试题做电商和做视频的公司出题侧重点有明显的差别。爱奇艺的业务核心是视频播放这意味着他们的研发工程师日常工作要跟CDN节点调度、视频转码、弹幕实时推送、推荐系统这些东西打交道。所以二这套卷子里有一些题目表面上是一道通用的算法题实际考察的是你在视频场景下的技术敏感度。举个例子题目里如果出现LRU缓存淘汰那绝对不只是考察你“哈希表双向链表”的实现能力而是希望你能联想到视频App里封面图缓存、播放器预加载数据块这些真实场景。再比如问到限流算法脑子里要立刻浮现出“高并发下播放请求如何保护后端服务”的问题。这要求你在做题的时候不能只盯着代码要把题目放回真实的业务环境里去理解出题人的意图。2. 代表性题型拆解算法与数据结构2.1 链表题反转链表的两种写法与递归陷阱链表反转是这套卷子里几乎必考的一道基础题。题目通常长这样给定一个单链表的头节点实现一个函数返回反转后的链表头。这道题有迭代和递归两种标准解法两种都需要掌握因为面试官很可能让你写完一种再写另一种。迭代法思路很直接维护两个指针一个指向当前节点的前一个节点一个指向当前节点每次把当前节点的next指向前一个然后整体右移。核心代码长这样struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode *prev nullptr; ListNode *cur head; while (cur) { ListNode *nextNode cur-next; // 先保存下一个节点防止断链 cur-next prev; prev cur; cur nextNode; } return prev; // 最后 prev 就是新链表的头 }这里最容易被忽略的一个细节是必须先用一个临时变量保存下一个节点再把当前节点的next指向前一个节点。我见过很多考生在面试现场一时紧张直接写cur-next prev结果后面的节点全丢了链表只留下了第一个元素。这个动作看似简单但很考察对指针操作的肌肉记忆。递归法虽然代码更短但理解起来比迭代法绕一些很多人在“递归之后需要做什么”这一步卡住了ListNode* reverseListRecursive(ListNode* head) { if (head nullptr || head-next nullptr) { return head; // 递归出口空链表或只剩一个节点 } ListNode *newHead reverseListRecursive(head-next); head-next-next head; // 让下一个节点的 next 指向当前节点 head-next nullptr; // 断开当前节点原本的 next return newHead; }递归解法的核心逻辑是假设后面的链表已经反转好了那么当前节点要做的就是把“原本的下一个节点”的next指向自己同时把自己的next置空。这个过程是从链表尾部倒着往回走的所以如果你没有画图辅助理解很容易在head-next-next head这一步懵掉。我建议平时练习时先画一个三节点的链表把每一层递归的返回值标出来比空想要快得多。复杂度方面迭代法是O(n)时间、O(1)空间递归法也是O(n)时间但空间复杂度是O(n)因为递归调用栈会占用额外空间。如果面试官追问“链表特别长的时候选哪种”正确答案是迭代法因为递归可能触发栈溢出。这也是视频公司考察代码是否工程可用的一个小信号。2.2 滑动窗口最长无重复子串的三种解法演进字符串相关题目在2016年前后的研发笔试里几乎是标配最长无重复子串就是最典型的一道。题面一般是给定一个字符串找出其中不含有重复字符的最长子串的长度。第一次看到这道题最容易想到的是暴力法枚举所有子串逐个检查是否有重复字符。这个思路复杂度在O(n^3)甚至O(n^4)字符串稍微长一点就完全没法用。笔试里如果只写出这种方案基本可以确定算法部分是拿不到高分的。较优的解法是滑动窗口哈希表用两个指针维护一个窗口右指针不断向右扩展遇到重复字符时左指针跳到该字符上一次出现位置的下一个位置。我用C写了一个可以直接套用的版本int lengthOfLongestSubstring(string s) { unordered_mapchar, int lastIndex; int left 0, maxLen 0; for (int right 0; right s.size(); right) { char c s[right]; if (lastIndex.count(c) lastIndex[c] left) { left lastIndex[c] 1; // 窗口左边界直接跳到重复字符之后 } lastIndex[c] right; maxLen max(maxLen, right - left 1); } return maxLen; }这段代码里特别容易出错的地方是lastIndex[c] left这个判断。很多初学版本少了这个条件导致窗口左边界被错误地往回跳结果算出来的长度比实际值大。原因是你可能遇到一个字符它上次出现的位置在窗口左边界之前也就是它已经不包含在当前的窗口里了但因为哈希表里还存着旧索引某些简单实现会误判成重复。这个边界条件可以说是一道“送命题”写对的人和不写对的人差距一眼就能看出来。我整理了一下这道题三种解法的差异方便你直观对比解法时间复杂度空间复杂度适用场景暴力枚举O(n^3)O(1)仅用于理解题目笔试不推荐动态规划O(n)O(n)可解但需要额外数组记录状态滑动窗口哈希表O(n)O(字符集大小)笔试和面试的最优解这道题在爱奇艺这套卷子里的意义很有意思它本质上是一种“数据流里找连续无冲突区间”的思想跟视频播放时统计无卡顿时间段、弹幕内容连续去重这类场景是很接近的。如果你在答题时能主动提到这种关联会让阅卷人对你的工程感觉留下更好的印象。3. 语言与底层机制C/C研发岗必考题3.1 虚函数与多态的实现原理不只是背概念C相关的题目在2016年爱奇艺研发笔试题里占了一个很大的板块。其中虚函数是雷打不动的考点题目通常不会直接问“什么是虚函数”而是问“虚函数是怎么实现多态的”或者“析构函数为什么需要是虚的”。要答好这类题必须先理解C对象模型。简单来说当一个类里包含至少一个虚函数时编译器会为这个类生成一张虚函数表vtable表中按声明顺序存放该类所有虚函数的地址。每个对象内部会多出一个虚指针vptr指向这个类的虚函数表。对象构造时vptr被初始化通过basePtr-virtualFunc()调用时程序运行时根据vptr找到正确的函数地址去执行这就是动态绑定。很多人背熟了这套解释但一到手写示例就露馅。我给你一个最精简的代码示例class Base { public: virtual void show() { std::cout Base::show std::endl; } virtual ~Base() default; // 基类析构函数必须是虚的 }; class Derived : public Base { public: void show() override { std::cout Derived::show std::endl; } }; void callShow(Base* b) { b-show(); // 这里到底调用哪个版本由 b 指向的真实对象决定 }关键回答点在于调用callShow时编译期并不知道传入的是Base对象还是Derived对象但通过对象内部的vptr去查找虚函数表运行时就能准确跳到Derived::show。这就是“一个接口多种实现”的实现机制。关于“为什么析构函数需要是虚的”我用一个很直白的场景解释假设你有一个Base*指针指向的是一个Derived对象当你对它执行delete时如果析构函数不是虚的编译器只会按Base的析构函数来清理Derived中额外的资源比如堆上分配的内存、打开的句柄就没有机会被释放这就造成内存泄漏。虽然这不一定立刻让程序崩溃但跑久了问题就会累积爆发这在服务端开发里是很严重的事情。与之相关的还有一个高频追问构造函数或析构函数里能不能调用虚函数答案是可以调用但是不会实现多态也就是说在构造函数里调用虚函数只会调用当前类自己定义的版本不会去调用派生类的重写版本。原因也很简单构造派生类对象时基类构造函数先执行此时派生类部分还没有被构造出来vptr还指向基类的虚函数表所以不可能实现多态。这种机制是为了避免在派生类成员未初始化时就调用它的方法造成未定义行为。如果面试官问到这里你能把这个原因讲清楚基本就是满分回答了。3.2 内存对齐手算结构体大小的正确姿势C笔试题里结构体大小计算也是高频送分题但很多人在细节上丢分。这类题考的是内存对齐也就是编译器在给结构体成员分配内存时会按“对齐数”进行填充而不是默认地连续分配。我直接拿一道典型的题目来说struct Test { char a; // 1 字节 int b; // 4 字节 char c; // 1 字节 };问你sizeof(Test)是多少很多新手会不假思索地答6141但实际在默认4字节对齐环境下答案是12。为什么我要给出完整的手算过程首先结构体的起始地址按最大对齐数对齐这里最大对齐数是4int占4字节第一个成员a是char占1字节位于偏移0第二个成员b是int要求偏移必须是对齐数4的整数倍所以a后面需要填充3个字节b放在偏移4到7的位置第三个成员c是char占1字节放在偏移8到现在结构体已经用了9字节但结构体总大小必须是对齐数4的整数倍所以还要在末尾填充3个字节最终是12字节。如果换一种声明顺序把int放在最前面struct Test2 { int b; // 4 字节 char a; // 1 字节 char c; // 1 字节 };手算一下b在偏移0到3a在偏移4c在偏移5合计6字节再对齐到4的整数倍结果是8字节。同样的三个成员只是顺序不同结构体大小就从12变成8这在实际工程中不是小事尤其是存储大量结构体对象时内存占用能差出三分之一。这也是出题人想考察的点你有没有对内存布局的敏感度。这里还要注意一个细节如果结构体里含有double或long long这类8字节成员对齐数通常就是8结构体总大小要对齐到8的整数倍。如果你想改变对齐方式C里可以用#pragma pack(n)但在绝大多数笔试题里默认对齐就是“按所有成员中最大的对齐数对齐”。我建议考场上一旦遇到这种题先在草稿纸上画出每个成员的偏移位置不要凭感觉直接加踩过的坑多了你就知道手算一步都不能省。4. 网络与操作系统视频公司的“隐藏加分项”4.1 TCP三次握手为什么不是两次或四次网络部分的考题最经典的莫过于TCP三次握手。爱奇艺这类视频公司对网络知识的考察格外重视因为视频传输质量直接取决于对TCP/UDP、拥塞控制、连接管理等机制的掌握程度。常见考法是描述三次握手的过程并说出为什么需要三次而不是两次。三次握手的过程大家应该都背得下来客户端向服务端发送SYN报文进入SYN_SENT状态。服务端收到SYN回复SYNACK报文进入SYN_RCVD状态。客户端收到SYNACK再回复ACK报文进入ESTABLISHED状态服务端收到ACK后也进入ESTABLISHED状态。难点在“为什么不是两次”。这个问题的关键不是“确认双方都能收能发”这种笼统的说法而是“防止历史重复SYN导致的连接混乱”。我打个比方假设客户端发送了一个SYN报文因为网络拥堵这个SYN被延迟了很长时间客户端以为它丢了于是重新发送了一个新的SYN。如果只握手两次服务端收到第一个延迟SYN后就会建立连接并分配资源但实际上客户端根本不想建立这个连接了这就会造成服务端资源浪费甚至连接出错。而三次握手中客户端在收到服务端的SYNACK后能够识别出这个SYNACK对应的不是自己期望的SYN序号于是发送RST报文拒绝连接服务端就会释放资源。所以第三次握手本质上是在干一件事让服务端确认“客户端确实收到了我的响应而且建立连接是客户端主动且真实的想法”。很多考生会把“三次比两次安全”理解成“多一次多一份保障”这样答不够准确会让人觉得你只是背了结论。我当时复习的时候把这个逻辑跟视频播放场景联系在了一起视频App每个播放请求都要建立至少一条HTTP连接在弱网环境下如果你不了解TCP握手的可靠性机制就很难理解为什么有时候接口会卡住也无法解释为什么重试能解决问题。这种结合业务场景的理解方式比单纯背八股文要高效得多。4.2 HTTP状态码与DNS视频App排障的基本功除了TCP网络部分还常考HTTP状态码和DNS解析过程。这部分在爱奇艺研发岗里属于“必须拿分”的基础题因为日常排查线上问题基本就是看状态码和查域名解析。HTTP状态码我建议你按分类记忆而不是一个一个背。2xx表示成功需要重点关注200正常和206部分内容视频分片播放时很常见3xx表示重定向要重点关注301永久重定向和302临时重定向这对理解播放地址调度很重要4xx表示客户端错误常用的是400请求错误、401未认证、403无权限、404资源不存在5xx表示服务端错误常见的包括500内部错误、502网关错误、503服务不可用、504网关超时。在这套题的场景里有一个很典型的追问是视频播放器请求一个分段视频文件返回304是什么含义这代表资源没有修改可以命中浏览器缓存学名叫“Not Modified”。这个问题背后的意图在于对于视频点播来说缓存命中率直接影响播放启动速度和CDN带宽成本。如果你能答出304的含义再顺便提一句“可以用ETag或Last-Modified配合If-None-Match来做缓存验证”阅卷人就会认为你有实际的HTTP调优经验。DNS解析过程也要能完整讲下来浏览器先查本地缓存查不到就查操作系统缓存再查hosts文件如果还没有就向本地DNS服务器发起递归查询本地DNS服务器会向根DNS服务器查询顶级域名服务器的地址再向顶级域名服务器查询权威DNS服务器的地址最后从权威DNS服务器得到域名对应的IP。整个过程里缓存层级的设计是核心因为每减少一层查询就能降低一个数量级的时延。视频App里大量使用CDN加速一个视频域名的DNS解析结果往往不是单一的IP而是CDN调度系统根据用户地理位置、运营商等条件返回的多个节点地址这就是为什么解析过程本身就能影响播放体验。答题时如果能把DNS和CDN调度联系起来会让你的答案在众多考生中脱颖而出。4.3 进程与线程、死锁四条件用弹幕服务的例子就能想明白操作系统方面进程和线程的区别、死锁产生条件几乎每年都考。这些概念单独背都不难但往往一道大题会把它们串在一起考察你能不能从系统角度分析问题。进程和线程的核心区别我用一句话概括进程是资源分配的基本单位线程是CPU调度的基本单位。进程拥有独立的地址空间和系统资源线程共享所属进程的地址空间和大部分资源但每个线程有自己的栈和寄存器上下文。由此可以推出一系列结论进程间互相隔离一个进程崩溃不影响其他进程线程间通信更高效但同步不当就容易出并发问题。死锁的四个必要条件不用死记硬背用弹幕服务举个例子就能串起来假设有两个线程A和B都要同时访问“用户信息缓存”和“弹幕消息队列”两个资源。线程A先锁住用户信息缓存再去锁弹幕消息队列线程B先锁住弹幕消息队列再去锁用户信息缓存。这时候如果A持有了用户信息缓存等队列B持有了队列等信息缓存两个线程就会无限期地互相等待这就是死锁。四个必要条件是互斥条件资源同一时刻只能被一个线程占用、占有且等待条件线程占着已分配资源同时还在等新资源、不可抢占条件资源不能被强制从线程手中拿走、循环等待条件多个线程之间形成一个等待环路。破局的方法也很清晰破坏任意一个条件就能避免死锁工程里最常用的是破坏循环等待也就是给所有资源规定一个全局锁顺序所有线程都按同样的顺序去加锁。这里有个实际代码技巧如果你用C11及以上std::lock可以同时锁住多个互斥量避免因加锁中间状态导致死锁比手动按顺序加锁更安全。回答这种问题的时候能举例说明并且给出工程解法比干巴巴地背概念要有力得多。5. 回答策略与常见失误把自己当阅卷人来看这套题5.1 编程题该先写思路还是直接撸码很多人一拿到编程题恨不得立刻就开始敲代码生怕时间不够。但以我后来参与出题和阅卷的经验看先写思路的人往往得分更高。原因很简单研发笔试考察的不只是最终代码还包括你的解题过程。你在代码旁边用自然语言写清楚“我打算用滑动窗口因为……”“这里用哈希表是为了把查找从O(n)降到O(1)”阅卷人一眼就能看出你的思考深度即使代码有bug思路分也能捞回来一些。我推荐的答题顺序是先在草稿纸上画出算法流程标出关键数据结构和边界条件然后动笔写主函数最后再补上代码注释。注释不需要写废话而是在关键逻辑处用一两句话说明“这一步为什么这么处理”。比如链表反转代码里那句“先保存下一个节点防止断链”就是阅卷人最想看到的注释。这比你在开头写一大段“本函数的作用是将链表反转”要有用得多。不要担心写思路会浪费时间。对绝大多数编程题来说完整思考一遍再写代码通常比边想边写更快因为纠结和返工的时间往往比规划的时间更长。我实测下来一道中等难度的算法题先写思路再编码比直接上手平均能节省5到10分钟。5.2 这套卷子的合理时间分配研发工程师笔试题二这类卷子题量一般在10到15道之间时间通常是60到90分钟。我按常见情况给出一个80分钟的时间分配参考你可以根据自己的强项做调整时间段内容说明前5分钟通读全卷标记题型不要急着做题先把整个卷子看一遍区分“拿分题”和“攻坚题”第5到35分钟语言基础操作系统网络选择/简答题这些题通常不需要写大段代码快速拿分第35到65分钟编程题留出核心时间段每道编程题最多25分钟最后15分钟检查补全思路描述检查边界条件、变量名、漏答的小题这里最忌讳的是在前面的简单题上反复斟酌导致编程题只剩十分钟。编程题的分值通常占整张卷子的40%以上是拉分的关键无论如何都要给它留足时间。如果有一道编程题卡了很久没思路我建议先跳到后面会做的题回头再来看这道题往往会有新的灵感。5.3 爱奇艺这套卷最容易出现的5个失误我把平时在相关复盘帖、面经里高频出现的失误整理成了一张表你在考前可以对着自查失误类型常见表现如何避免边界条件遗漏空链表、空字符串、只有一个元素的情况没有考虑写代码前先列出所有边界输入逐个去代码里验证哈希表重复判断错误滑动窗口左边界跳错导致结果偏大牢记判断条件必须结合当前窗口左边界位置只写代码不写注释代码逻辑正确但阅卷人看不出你的思路在关键步骤处加上简短注释说明原因时间复杂度分析错误答不出自己的算法是O(n)还是O(n^2)养成分析循环层数和数据操作的次数放弃画图遇到复杂指针问题全靠脑补容易绕晕草稿纸上画出节点指向图再动手写代码这五类失误里最常见也最可惜的是第二类。一个字符判断的错误就能让原本O(n)的算法变成错误答案而这类错误如果自己没跑测试用例几乎不可能发现。所以我做笔试题时有个习惯代码写完后一定手动用一个正常输入、一个边界输入、一个重复较多的输入去“脑内执行”一遍基本能排查掉90%的潜在bug。6. 一些私房话从这套题延伸出的备考思考说实话2016年的这套题放在今天来看有些考点已经发生了变化比如现在更流行的语言是Go和Python系统设计题的比重也比当年大了不少。但基础部分的核心逻辑没有变公司想找的永远是那个能把技术原理讲明白、能写干净代码、能解决真实问题的人。我复盘这套题最大的感受是刷题不能只图“做过”。每一道题做完之后都应该追问自己三个问题这道题考的是哪个知识点我解的思路有没有更优版本如果把它放到真实业务场景里对应的是什么问题举个例子当你做完最长无重复子串的滑动窗口解法你可以顺势想一想如果我把窗口比作视频播放器里的一段播放记录把重复字符比作重复的请求ID那这道题就变成了“如何在一段时间内统计不重复的用户请求数”。这种思考方式才是研发笔试真正想考察的东西。还有一点想特别提一下不要在笔试前突击看一堆偏题怪题把经典题型的原理吃透远比多刷一百道题更重要。爱奇艺这套卷子里没有任何一道题是超出课本范围的但它就是有本事在看似普通的题里拉开差距靠的就是对细节和原理的追问。最后分享一个小技巧。我建议你拿到任何一套笔试题先别急着从头做到尾花两分钟把整个卷子的题型分布扫一遍在心里给每一道题标上“稳”“中等”“难”三个标签然后先做稳的再做中等难度的最后攻克难题。这套策略在时间紧张的笔试现场尤其好用能帮你稳定心态也更容易把该拿的分全部拿到手。毕竟笔试不是让你证明自己无所不能而是在有限时间里展现出你最靠谱、最值得信任的一面。
返回列表