ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

第四范式校招后端笔试题复盘:算法与系统设计深度解析

第四范式校招后端笔试题复盘:算法与系统设计深度解析 第四范式2019校招后端笔试题我刷完后的复盘和思考2019年秋招季我投了第四范式的后端开发岗。那时候AI公司风头正劲第四范式以“机器学习平台”“AutoML”被大家熟知我当时想着后端笔试肯定不只是普通的Java/Go八股文多少会沾点算法、系统设计甚至一点机器学习基础。等真拿到题目我发现这判断对了一半它对基础工程能力的考察远比我想象的“扎实”有些题甚至让我当场冒汗。这篇复盘整理了题型分布、核心考点、几道典型题的完整解题思路还有我踩过的坑给后面准备AI公司后端岗的同学做个参照。这套题适合两类人看一类是像我一样准备AI公司后端实习/校招的想提前了解考察方向和难度另一类是已经在做后端开发、想对照自检一下自己基础知识是否经得起“笔试题”这种压力测试的。整份卷子的风格不是让你死记硬背而是把知识点放到具体场景里考察你在限定时间内能不能快速给出可落地的方案。1. 整体印象这卷子不是“刷题就能过”的类型先说整体体感。整套题大致分了几个模块选择题/填空题、算法编程题、简答题/方案设计题。如果只看算法题难度其实不算变态LC中等偏上一点但你如果以为把LeetCode刷熟就稳了那就错了。真正拉开差距的是后面的方案设计题和与机器学习场景结合的考察点这些题没有标准答案靠的是你平时写代码时有没有形成一套系统的思考框架。我印象很深的一点是试卷里反复出现“海量数据”“高并发”“缓存一致性”这些关键词比一般互联网公司后端题更侧重数据处理和模型服务的场景。后来查了下面试资料第四范式主要做企业级AI平台后端需要支撑模型训练、模型上线、在线推理这些链路所以它的后端工程师本质上是在“AI基础设施”上做工程。这直接决定了笔试题会往“系统设计基础原理算法基础”三个方向出。另外时间安排上要提醒一下总分值不算吓人但题量不小。我当时拿到卷子先扫了一遍发现编程题有3道每道都不是5分钟能写完的还有两道设计题需要写很多文字。如果你按顺序硬做很容易在中间消耗太久最后设计题只能草草写两句。合理策略是先把容易拿分的选择题和简单题快速过掉再集中火力攻克编程题和设计题。2. 题型拆解考点分布与考察意图那次笔试总共大概9到10道大题细分下来可以归纳成几条线。我按记忆整理了一下各部分的倾向性大概是这个感觉考察方向典型内容占比感受为什么这么考数据结构与算法排序、二分、DP、链表、二叉树、堆35%校招基础筛选线确保候选人有一定编码功底操作系统与网络进程线程、内存管理、TCP/UDP、IO模型20%后端要面对高并发和网络IO底层原理必须通透数据库与缓存索引原理、事务隔离、Redis数据结构与淘汰策略15%存储是后端核心尤其要处理海量训练/推理数据机器学习基础评价指标、过拟合、特征工程、模型上线中的工程点15%公司基因后端也要能和算法团队同频沟通场景设计题缓存设计、任务调度、分布式链路、QPS估算15%考察真正的系统设计潜力和工程落地思维这个分布很能说明问题它对“算法功底”和“工程深度”是并重的。普通的后端笔试更偏“你能不能在LeetCode上写出题”而这份卷子还希望你回答“为什么这样设计”“数据库底层是怎么实现的”“模型上线时精度和延迟怎么权衡”。我当时就栽在一道和“模型上线”相关的选择题上它问的是模型在线推理服务需要重点关注的性能指标。选项里有AUC、QPS、延迟、召回率这类我直接选了AUC后来复习时才发现面向线上推理最核心的是延迟Latency和吞吐QPSAUC是离线评估指标。这说明一个点如果你是纯后端背景最好提前了解一下机器学习的基本概念哪怕只是知道“离线指标”和“在线指标”的区别也能避开这种基础送分题的坑。3. 核心知识点深度拆解光刷题不够要懂底牌我在复习阶段最大的感受是光看面经和刷题很容易陷入“背答案”的状态。但第四范式的笔试题尤其是选择题和简答题经常换一个场景问同一个原理考察你是不是真的理解。下面我把考察频率最高的几个知识点拆开来说这部分也是我认为整份卷子最值得研究的地方。3.1 数据结构与算法不是最难的但最容易失分算法题大概在LC中等难度但会结合工程场景出题。比如有一道近似“实现一个带过期时间的LRU缓存”虽然核心是“HashMap双向链表”但加了“过期时间”和“并发访问”两个约束瞬间就变了很难。只背过LRU基本模板的话很容易在“过期清理”和“线程安全”上踩坑。我当时用double check locking写的加了一个后台线程做过期清理交卷后复盘发现如果要求“读操作也更新过期时间”我的实现就错了因为我在get的时候没有维护每个key的最后访问时间。另一道比较有代表性的题是海量数据topK从100亿个整数中找出最大的1000个。看到“100亿”先别慌这题考点不是分布式而是“局部最优”和“堆”这两个点。主体思路是维护一个大小为K1000的小顶堆。每来一个数如果比堆顶大就替换堆顶然后向下调整如果比堆顶小直接丢弃。因为小顶堆堆顶是堆里最小的元素如果当前数字连堆顶都打不过说明它一定不在最大的K个数里。这样处理一个数的时间是O(logK)总复杂度是O(n logK)对100亿数据来说就是“读一遍 每个数做几次比较”。这个思路和大数据平台里map端combiner的局部聚合很像每个节点先算局部topN再汇总到主节点算全局topN。所以与其说这题考算法不如说考你“有没有大数据处理的分而治之意识”。3.2 操作系统与网络笔试里的“隐形大坑”操作系统和网络在选择题和简答题里占了不少分而且考察点非常基础但越是基础越容易忽略。比如多线程、进程线程区别、用户态与内核态切换、零拷贝、epoll和select的区别这些概念看似大家都会背但是换个方式问就很容易含糊。有一道题大概是问多线程并发访问同一个变量存在什么问题解释下volatile和原子类的区别。这种题目在校招里真的一抓一大把因为后端工程师写高并发代码离不开线程安全。但很多人答题时就只写“volatile保证可见性不保证原子性”然后没下文了这道题分拿不高。要拿高分你需要进一步展开volatile底层是基于内存屏障实现的线程写变量时会强制刷新到主内存读变量时从主内存拉取。它解决的是可见性但没解决“read-modify-write”这种复合操作的原子性。而AtomicInteger是基于CAS自旋实现的它通过比较并交换的方式保证复合操作原子性。如果你能顺便写一句“CAS在竞争激烈时可能导致大量自旋所以高并发下可以考虑LongAdder”那这道题的区分度就出来了。网络方面TCP三次握手、四次挥手是必然考的但第四范式比较偏爱的考点是TCP和UDP的区别 为什么在线推理服务推荐用HTTP/2而不是HTTP/1.1我当时看到这题有点懵因为它不是纯背八股而是要把网络知识结合到实际业务里。后来想明白了HTTP/2支持多路复用一个TCP连接里可以并行多个请求/响应不会像HTTP/1.1那样前面一个慢请求堵住后面所有请求。这对于在线推理服务很关键因为推理服务往往有几十毫秒甚至上百毫秒的响应时间如果配置为“一个连接同时只能处理一个请求”QPS就会被严重限制。3.3 数据库与缓存你必须懂索引不能只看面试题数据库这块近几年越来越热门笔试不考写SQL而是考索引原理。比如为什么用B树做索引而不用红黑树或哈希表这题算基础中的基础但想答得有深度也不容易。哈希索引适合等值查询但遇到范围查询就废了红黑树虽然能范围查询但在磁盘场景下树太高一个节点一个磁盘块查找时磁盘IO次数太多。B树把多个键值放在同一个节点里每个节点对应一个磁盘页大小通过增加每个节点的“扇出”把树高控制在三层左右一般三次磁盘IO就能从根节点走到叶子节点。而且B树的叶子节点用双向链表串起来查“age between 20 and 30”这种范围查询时只要先找到20然后顺着链表向后遍历就行。我当时在卷子上写“B树叶子节点串成链表适合范围查询”后又补了一句“所以在建普通索引时要尽量让索引列有顺序比如自增ID就比UUID更合适这样能减少索引页的随机IO和页分裂”。后面这句是我做题时的临场发挥但我觉得正是这类“原理延伸到实践”的点才是面试官想看到的。缓存方面Redis几乎算是后端笔试标配了重点考察Redis的过期删除策略、内存淘汰策略、持久化机制。有一题问的是Redis的key过期了为什么内存没有马上释放答案是Redis过期删除采用“惰性删除定期删除”结合当key被访问时检查是否过期过期则删除同时后台定时任务抽样删除部分过期key。如果你只答“惰性删除”说明你没看过Redis源码定期删除这个机制很多人会漏掉。还有一道设计题如何设计一个缓存避免缓存雪崩、穿透、击穿。这三兄弟是后端系统设计题里的“钉子户”。我当时分别写的解决方案是穿透布隆过滤器拦截不存在的key或者缓存空值比如缓存一个特殊标记设置短过期时间击穿使用互斥锁只有一个线程回源数据库其他线程等待或者用“逻辑过期”策略雪崩缓存过期时间加随机扰动避免大量key同一时间过期或者集群部署避免单点失效如果能把雪崩和穿透区分开再给出“为什么布隆过滤器适合解决穿透”这道题基本就稳了。3.4 机器学习基础后端为什么也要懂这些第四范式的笔试题里出现机器学习概念我一开始是有点意外的但仔细想想又很合理。后端工程师要写的并不只是“CRUD API”而是要支撑模型全生命周期包括数据集的读取与校验、特征处理流程的编排、模型训练任务的调度、模型上线后的在线推理服务。如果后端对机器学习的基本概念一无所知很难和算法工程师协作。这部分考得不算深基本都是概念理解。比如过拟合的解决办法在卷子上很常见正则化、增加数据量、dropout、早停、交叉验证。再比如评价指标的选择二分类模型在正负样本极不平衡时用准确率Accuracy是没有意义的因为全部预测为负样本可能也有99%的正确率这时候更应该看AUC、F1、召回率、精确率这类指标。我记得当时还考了一个很实际的问题模型训练完成后部署上线时如何评估模型性能变化选项里有“A/B测试”“灰度发布”“回滚机制”这些。答案是“A/B测试”因为它能在真实流量中对比新老模型的效果指标从而判断是否应该全量上线。这已经偏MLOps了。如果你只懂传统后端根本没接触过这些词很可能选错。我当时选对了但更多是靠直觉而不是系统性判断。所以给纯后端背景的同学一个建议准备第四范式这类公司不用啃完整个机器学习教材但至少要把以下概念过一遍监督/非监督学习、特征工程、过拟合/欠拟合、准确率/精确率/召回率/F1/AUC、训练集/验证集/测试集、模型上线中的A/B测试和灰度发布。这部分花一天时间就能补齐回报率非常高。4. 实操环节三道典型题从读题到AC的完整思路前面讲了知识点这节我用三道我印象最深的题完整还原一下从读题到解题的思考过程。我会把代码也用伪代码/代码片段写出来尽量还原当时的思路而不是直接给答案。4.1 算法题带过期时间的LRU缓存题目大意实现一个带过期时间的LRU缓存支持get(key)和put(key, value, expireTime)要求在平均时间复杂度O(1)内完成。读题时我脑子里立刻浮现出一串问题O(1)的查找怎么实现HashMap可以。O(1)的淘汰怎么实现双向链表可以链表末尾就是最久未使用的节点。过期时间怎么处理key-value扩展为key-nodenode里存value和expireTime。几个关键设计点双向链表中每个节点除了存key、value还要存expireTimeget时如果当前时间超过expireTime直接删除并返回不存在get时如果命中且没过期需要把该节点移动到链表头部put时如果key已存在更新value和expireTime移动到头部如果不存在插入头部如果容量超了删除尾部节点初次实现比较简单但还是要注意几个工程细节用“当前时间戳”作为判断依据而不是存“剩余有效期”因为存剩余有效期的话你还要在每次访问时做减法处理方式更绕清理过期时间的时候不能只清理“一个节点”理论上可以惰性在访问时发现过期再删这样避免后台线程额外消耗但如果不做主动清理缓存里可能会堆很多已经过期的key内存浪费如果只考算法惰性删除就够了。但如果考到并发建议用ConcurrentHashMap 自己加锁的双向链表或者直接使用Caffeine的guava实现做参考。当时时间有限我就用一个简单哈希表双向链表实现没有加并发控制把“线程安全”这点写在了注释里表示我知道这题还有这个维度。代码骨架大概是struct Node { int key, val, expireTime; Node* prev; Node* next; }; class LRUCacheWithExpire { private: unordered_mapint, Node* mp; Node* head; Node* tail; int capacity; public: int get(int key) { auto it mp.find(key); if (it mp.end()) return -1; Node* node it-second; if (node-expireTime currentTime()) { removeNode(node); mp.erase(key); return -1; } moveToHead(node); return node-val; } // put 类似记得在插入前先检查容量和过期 };真的把它跑通之后你会发现这类题的核心不是LRU的“双向链表”本身而是你怎么把“过期时间”和“访问顺序”这两个维度融合在一个数据结构里。后来我在写业务代码时也用到了这个思路给缓存key加了一个“业务维度的逻辑过期时间”用来防止旧特征数据被重复使用。4.2 算法题求数组中的逆序对数量题目大意给定一个数组求逆序对的数量。逆序对的定义是对于数组中的两个下标i j如果arr[i] arr[j]则它们构成一个逆序对。这题如果只知道暴力“双重循环”时间复杂度O(n^2)在大数据量下必然超时。正解是利用归并排序在merge的过程中如果左半边当前元素大于右半边当前元素说明左半边从当前元素到结束都和右半边这个元素形成逆序对直接累加。我在草稿纸上模拟了整个过程。比如[7, 5, 6, 4]归并排序会先拆成[7,5]和[6,4]然后继续拆成单个元素。合并[7]和[5]时发现75逆序对1变成[5,7]合并[6]和[4]逆序对1变成[4,6]合并[5,7]和[4,6]时因为54所以[5,7]里的两个元素都和4构成逆序对逆序对2然后76逆序对1总数就是11215。代码实现上归并排序框架不变只需在合并时加上计数int count 0; public void mergeSort(int[] arr, int left, int right) { if (left right) return; int mid left (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid 1, right); merge(arr, left, mid, right); } private void merge(int[] arr, int left, int mid, int right) { int[] temp new int[right - left 1]; int i left, j mid 1, k 0; while (i mid j right) { if (arr[i] arr[j]) { temp[k] arr[i]; } else { count (mid - i 1); temp[k] arr[j]; } } while (i mid) temp[k] arr[i]; while (j right) temp[k] arr[j]; System.arraycopy(temp, 0, arr, left, temp.length); }这题看似基础但它的工程意义在于任何需要“统计顺序关系”的场景都能用到归并排序的merge阶段。比如在实时特征计算中有排序特征的需求逆序对数量可以作为一个特征衡量序列的混乱程度所以这类题不是白考的。还有个小建议遇到数字类题目先用小样例手动演算一遍再用代码实现尤其在笔试场景下可以避免因为“边界条件处理错误”导致AC不了。4.3 设计题简答设计一个支持海量任务的异步处理系统题目大意系统每天要处理上千万个异步任务比如模型推理任务每个任务需要调用外部服务耗时不定偶尔失败需要重试要求设计一个高吞吐、可扩展的异步任务处理方案。这题没有标准代码但你需要给出清晰的架构和关键组件的选型理由。我当时分了几层来写第一层是生产者。调用方通过消息队列的API提交任务不要直接阻塞等待结果。消息队列选择上我写的Kafka因为它的吞吐量非常高适合大批量任务的导入。如果任务量中等但要求投递可靠RocketMQ也是一个选择它的事务消息和重试队列更完善。第二层是消费者/Worker。下游worker集群从消息队列拉取任务执行具体的模型推理或外部调用。这里要注意几点worker要做幂等因为消息队列是“至少一次”投递语义可能重复消费消费速度要可控不能一次性拉太多任务导致内存打爆worker失败后要重试但重试要有上限并且超过上限后进入死信队列方便人工排查第三层是结果存储。任务执行完的结果写入一个高性能的存储系统比如Redis或者单独的MySQL表调用方通过轮询或者回调拿到结果。第四层是监控与告警。每类任务的执行时间、失败率、堆积数量都要有指标因为异步系统“积压”是最隐蔽的故障。我写了一个“堆积延迟”作为核心监控项意思是队列里最老的消息等待了多久而不是简单看队列长度因为队列长度和队列消费速度未必成正比。这个架构思路其实很通用很多公司在做异步任务处理时都是这个套路差别在于消息队列选型、重试策略、以及是否使用分布式调度平台。准备这类设计题时建议多看几篇“消息队列Kafka/RocketMQ实战”的文章把消费幂等、重试机制、顺序消息这三个点吃透。5. 实际踩过的坑与复盘总结这节说点真实经验有些真的是考场上踩了才能记住的坑。5.1 时间分配别在算法题上死磕满分我当时花在第二道算法题上的时间太久了一直想在O(n)里解出来结果浪费了半个小时回头看如果用归并排序早就写完了。笔试不比竞赛不需要你提出最优的最优能保证通过所有测试用例、代码清晰可读就已经是很好的答案。遇到一道题卡了15分钟以上就应该果断标记一下先去做后面的题回头有时间再优化。5.2 边界条件数组为空、只有一个元素、输入超大我复盘时发现自己很多失分不是不会做而是边界条件没处理好。比如快排没有处理数组为空的情况LRU缓存没有处理容量为0的情况。在笔试环境里测试用例往往故意包含这些极端情况。解决方法是写代码前先想清楚空输入、单元素输入、重复元素、最大数值范围这些边界是否都覆盖了。5.3 设计题别只说方案要带“为什么”很多同学和我初期的状态一样写设计题只会堆名词用Redis做缓存、用MQ做解耦、用分库分表。但你得问自己一个问题为什么选Redis而不是本地内存为什么选Kafka而不是ActiveMQ为什么分库分表的键要这么分比如Redis适合高并发读场景因为单线程模型避免了锁竞争但在一致性要求极高的场景就未必合适因为它默认无法保证强一致。如果你能主动点出“这里可以接受短暂不一致所以用Redis缓存”面试官就会知道你是真的懂。5.4 代码风格模拟面试环境不要依赖IDE提示很多人在IDE里写代码很顺一旦到了纯文本框的笔试环境连括号匹配都费劲。我建议平时练习时直接在网页端的编辑器里刷题不开代码补全、不点“智能提示”完全手写。这个过程很痛苦但非常有效尤其能锻炼你组织代码块段的能力。你在试卷上每写一个for循环都能少纠结一次“该用i还是j”就是一种进步。5.5 操作系统/网络概念不要只背结论比如“epoll和select的区别”在网上看到了很多面经每次都觉得自己懂了但如果你不去实践一下写个echo server其实根本无法深层次体会它为什么减少了系统调用。笔试可能只考一个选择但如果你能把“select是轮询所有的fd集合epoll是事件驱动、只返回就绪的fd、且不需要每次重新传入fd集合”这个机制说清楚你就比大多数人强。6. 从这套题反推AI公司后端到底在招什么人复习和复盘这套题时我一直在想一个问题第四范式的后端笔试到底想筛选出什么样的人后来我得出的结论是它想找的不是“纯刷题机器”也不是“只会写业务CRUD的码农”而是那种“基础扎实、能思考、能落地”的技术人。基础扎实体现在数据结构、操作系统、网络、数据库这些底层原理的掌握是否真正通透而非死记硬背。能思考体现在设计题里能否给出方案并且有条有理地讲清楚取舍。能落地体现在代码是否考虑边界条件、异常场景、并发安全这些“工程细节”。你自己也可以对照这个标准自测一下如果你现在写一个带token校验的HTTP服务能立刻想到需要处理并发访问问题吗如果你设计的缓存系统能明确说出它在不同一致性要求下的应对方案吗如果你的算法题都能过但设计题聊聊就卡壳那说明你离“真正的后端工程师”还有一段距离。最后分享一个我自己从这次笔试中学到的小技巧在校招准备阶段做任何一道题都先问自己三个问题——这个知识点在真实工程中解决什么问题如果数据量扩大100倍会怎样如果要和其他系统协作接口要怎么设计练到后面你会发现自己不只是在“答题”而是在“做一个系统”这种状态对笔试和未来工作都有帮助。
返回列表