ARTICLE DETAIL

资讯详情

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

2017猿辅导后端面试复盘:字符串压缩、拓扑排序与系统设计

2017猿辅导后端面试复盘:字符串压缩、拓扑排序与系统设计 1. 为什么2017年的猿辅导面试题值得翻出来重看一遍先交代一下背景。2017年在线教育正处在第一波野蛮生长的尾巴上猿辅导当时已经从题库工具转型为在线直播辅导平台主打K12大班课和双师模式。那一年校招的技术面试题和后来大家熟悉的“纯刷题”风格有明显区别——它已经开始把算法题往教育场景里塞同时又保留了对基本功的严格考察。翻出下午场这份题复盘不是为了考古而是因为里面好几道题放在今天依然是高频变体尤其是字符串处理和拓扑排序在推荐系统里的变形现在大厂面试依然在考。先说我自己的情况那时候我在北京某高校读研二投的是猿辅导的后端开发岗。下午场一共四轮三面技术加一面HR整体节奏比上午场紧凑得多。我印象最深的不是题有多难而是面试官几乎每道算法题后面都会追问一句“这个场景如果在我们的上课系统里你会怎么改”。这种追问方式在2017年的校招里算比较超前的现在想想其实是他们在提前筛选那些能理解教育业务逻辑的工程师。这篇复盘我按时间线来写把每一轮遇到的问题、我当时的回答思路、以及事后复盘时补充的解法都整理出来。中间会穿插一些当时同一批面试者反馈回来的题目方便大家对照。适合正在准备校招的同学阅读也适合工作两三年的朋友拿来自查基础。2. 下午场开局一道看似是送分题的字符串转换下午场的面试官是个看起来比我们大不了几岁的年轻人上来没有废话直接在白板上写了第一道题“给定一个字符串把其中连续出现的字符按‘字符出现次数’压缩比如aaabbc变成a3b2c1如果压缩后长度没有变短返回原字符串。”这道题本身不难属于字符串处理里的入门级题目LeetCode上也有原题。但我当时明显感觉面试官在观察两件事第一你会不会处理边界条件第二你写代码的时候有没有意识去讨论空间和时间复杂度而不是直接闷头写。我当时用了最直观的解法一次遍历用一个计数器记录当前字符连续出现的次数当字符变化时把上一个字符和次数拼到结果里。这里有个很容易踩的坑——字符串末尾的那组字符遍历结束后需要单独处理一下很多人在面试时写到一半会忘记这个收尾步骤。事后复盘时我整理了一个更规范的版本这里用C写一下std::string compressString(std::string S) { if (S.empty()) return S; std::string res; int count 1; for (int i 1; i S.length(); i) { if (S[i] S[i - 1]) { count; } else { res S[i - 1] std::to_string(count); count 1; } } res S.back() std::to_string(count); return res.length() S.length() ? res : S; }面试官追问的第一个问题是“如果压缩后长度没有变短返回原字符串那你是先完整压缩再比较还是可以在压缩过程中提前终止”这是一个优化点。如果压缩过程中当前结果长度已经超过了原字符串长度就可以提前返回原字符串不需要继续遍历这在面对超长字符串时能省下不少时间。第二个追问更有点意思“如果这个字符串不是ASCII字符而是UTF-8编码的中文压缩逻辑会有什么变化”当时我对这个问题答得不够好只说了“中文一个字符占多个字节length()返回值可能和字符数不一致”。现在我想补充得更准确一些在C里std::string::length()返回的是字节数对于UTF-8编码的中文字符每个字占3个字节所以如果直接用length()来判断字符是否相同会得到错误结果。正确做法是把字符串切成Unicode码点数组再处理或者用支持Unicode的库函数。面试官真正想考察的是你写代码时是不是只满足于“能用”而忽略了字符编码这个工程上躲不开的问题。这道题给整个下午场定了一个基调题目难度不会太离谱但每一道后面都有场景化的追问考察的是“你会不会”和“你能不能想到实际生产环境里会遇到什么问题”的差距。3. 二面算法题课程依赖关系与拓扑排序的现场应用第二道题出现在二面题目大概是这样的“猿辅导的课程体系里有些课程需要先修课程比如《高等数学》是《线性代数》的先修课现在给你课程总数和一系列先修关系对请判断是否可能存在一种学习顺序让所有课程都能被完成。”这正是拓扑排序的经典题。面试官给了一个有向图的场景——课程是节点先修关系是有向边。如果图中存在环就说明存在循环依赖无法完成所有课程。我当时的思路是使用Kahn算法也就是基于BFS的拓扑排序。先统计每门课的入度然后把所有入度为0的课程入队依次出队并减少后续课程的入度如果最后出队的课程数量不等于总课程数说明有环。这里我补充一下Kahn算法的核心代码用Java写public boolean canFinish(int numCourses, int[][] prerequisites) { ListListInteger graph new ArrayList(); int[] indegree new int[numCourses]; for (int i 0; i numCourses; i) graph.add(new ArrayList()); for (int[] p : prerequisites) { graph.get(p[1]).add(p[0]); indegree[p[0]]; } QueueInteger queue new LinkedList(); for (int i 0; i numCourses; i) { if (indegree[i] 0) queue.offer(i); } int visited 0; while (!queue.isEmpty()) { int cur queue.poll(); visited; for (int next : graph.get(cur)) { if (--indegree[next] 0) queue.offer(next); } } return visited numCourses; }写完基础版本之后面试官的追问方向从图论转向了业务“如果这些课程不是简单的先修关系而是有学时要求、有开课时间限制比如秋季学期只能开5门课你怎么调整你的算法”这道追问的核心是把拓扑排序升维到带约束的资源调度问题——不只是判断能不能完成还要给出排课方案。这个问题没有标准答案我当时给的思路是在拓扑排序的基础上增加一个贪心策略每次从入度为0的课程里优先选择学时最短的课排进当前学期这样可以尽量让每个学期的总学时均匀。后来我了解到这其实是“并行任务调度”问题的一个简化版本更严谨的做法是把课程按学期层数分层再用最大流或者动态规划来求最优解。面试官要的不是一个完美的答案而是看你遇到不确定的问题时能不能给出一个合理的思考路径并且在优化方向上展示自己的知识广度。第三道题就明显不那么“算法”了。面试官说“我们现在要设计一个小学生口算练习的功能用户每天练习20道题系统要根据用户的答题记录推荐明天练习的题目。请你说说你会怎么设计这个推荐策略。”这道题不在白板写代码纯聊天。考察的是推荐系统和教育产品结合的基本功。我当时提出的思路很简单用一个标签体系给题目打标比如“两位数加法”“退位减法”“乘法口诀”等然后记录每个用户在不同标签上的正确率和平均耗时。推荐时优先选择正确率在50%-80%之间的题目类别因为这类题目属于“跳一跳够得着”的难度太简单的题目没有练习效果太难的题容易打击积极性。面试官回了一句“你刚才说的这个思路和自适应学习里的‘最近发展区’理论基本一致如果让你用一个公式来表达推荐题目难度你会怎么设计”这个问题让我有点意外因为我没有系统学过教育学理论只能临时从数据角度去凑。我最后给出的回答是用一个加权公式把正确率、做题耗时、与上次练习的间隔时间组合起来作为难度调节的依据。正确率越低、耗时越长就越要降低题目的难度系数。这个回答虽然没有那么严谨但面试官至少看到了我是有逻辑地在思考问题。现在回头看这道题其实是整场面试里最贴合猿辅导业务的一道题。教育公司对工程师的要求不仅仅是写代码还需要对“学习效果”和“用户留存”这些业务指标有基本认知。推荐算法容错率低推荐错了用户就走人了。这一点在后来的工作里我体会越来越深。4. 下午场里的高频基础题与智力题不刷算法也能拉开差距第三面是一位看起来像架构师的面试官。这轮没有立刻上算法而是先问了一堆计算机基础题难度不大但覆盖面很广。我挑几道我觉得比较有代表性的列出来。第一道进程和线程的区别是什么为什么线程切换比进程切换开销小这类送分题里最容易翻车的是“开销小”的原因说不完整。线程切换开销小不仅是因为线程共享地址空间不需要切换页表还因为线程的上下文寄存器、程序计数器、栈指针比进程需要的上下文切换信息少——进程切换还要刷新TLB、更新内存管理相关的数据结构。只说“共享地址空间”只能得一半分。第二道TCP三次握手过程中第二次握手丢失了会发生什么这属于网络基础里比较容易被问偏的题目。第二次握手是服务器发送SYNACK报文如果这个报文丢失客户端会认为自己没有收到服务器的确认重发SYN同时服务器因为发送SYNACK后进入SYN_RCVD状态会因为没有收到客户端的ACK而超时重传SYNACK。这里的关键点是客户端和服务器会同时开始重传但重传的超时时间不一定相同。我最后补充了一句“TCP是双工协议两次握手的重传是独立发生的”面试官点了下头。第三道哈希表冲突解决方式有哪些除了拉链法和开放寻址法你还知道什么这题本身不难但我当时提到的一个点引起了面试官的注意——我说“拉链法在Java 8的HashMap里当链表长度超过8时会转为红黑树”他接着追问“为什么是8不是9也不是7”我当时没答上来。现在补充8这个阈值是综合考虑了随机哈希值的泊松分布概率和红黑树节点占用空间比链表节点大的因素在负载因子0.75的默认情况下链表长度超过8的概率极低设定为8既保证了树化不会频繁发生又避免了极端哈希冲突时链表过长导致的性能问题。基础题之后来了一道智力题“有两个瓶子一个能装3升水一个能装5升水水不限量如何准确量出4升水”这种题考察的是逻辑推理和逆推能力。我当时的思路是正向操作先装满5升瓶倒入3升瓶5升瓶剩下2升倒空3升瓶把2升水倒入3升瓶再装满5升瓶往3升瓶里倒因为3升瓶里已有2升只能再倒1升5升瓶里就剩下4升。整个过程需要操作6次。面试官追问“有没有更少步骤的方案”我一时没想到现在补充一个更简洁的版本装满3升瓶倒入5升瓶再装满3升瓶倒入5升瓶3升瓶剩1升倒空5升瓶把1升水倒入5升瓶再装满3升瓶倒入5升瓶。这个方案只需要4次倒水比我的第一版更优。这类智力题在面试里主要是看你的思路是否清晰以及遇到约束时能不能调整策略而不是纯粹考智商。基础题结束之后终于来了一道正经的手写代码题给定一个只包含括号字符串判断括号是否匹配。面试官说“这题很多人会写但很多人第一次提交都有bug”。写的时候我采取了比较稳妥的方式用一个栈遇到左括号入栈遇到右括号检查栈顶是否是匹配的左括号不是就返回false最后检查栈是否为空。这题的“坑”不是逻辑而是字符串长度为奇数时可以直接返回false的优化很多人在写这道题时不会想到这一步。我特意加了这一行面试官笑着说了句“还行”。5. 工程题系统设计题而不是纯算法题——在线课堂答题卡的提交方案第四面的题目明显往工程方向走。面试官在电脑上调出了一个简化的业务场景对我说“在线课堂上老师随时可以发一道选择题让学生作答学生可能在手机端也可能在PC端网络环境差异很大设计一个方案保证答案能可靠送达老师端。”这道题的母题是典型的“分布式场景下数据一致性”问题但被包装成了教育场景。我当时的回答分了三层。第一层是客户端学生按下提交按钮时先不要立即置灰而是进入“提交中”状态如果有失败就进入本地重试队列。这里的关键是防止学生因为网络抖动而重复提交。我给出的方案是每次提交带上题目ID、学生ID和一个客户端生成的唯一提交ID服务端用这个提交ID做幂等。第二层是服务端提交请求不直接同步写入数据库而是先写入消息队列再异步落库。为什么这么设计因为课上答题是一个瞬时高峰——一个班几千人同时提交答案如果全部同步写数据库数据库会扛不住。用消息队列做缓冲削峰填谷让消费端按自己能承受的速度去消费。第三层是异常处理学生提交答案后数据不是马上写到老师端展示而是有一个状态流转过程提交成功、处理中、展示成功。如果处理中状态超过一定时间需要一个补偿机制来重试。面试官听完之后重点追问了幂等和消息队列的细节。他问“如果学生手速快同一道题提交了三次你怎么保证老师端只看到一次”我说用唯一提交ID建唯一索引重复的提交直接丢弃。他又问“如果服务端要返回给客户端一个提交成功的确认而这个确认在网络里丢了客户端会重发服务端怎么知道这是重发而不是新提交”我回答服务端收到相同提交ID的请求时直接返回上一次处理的结果不重新执行。这在分布式系统里叫“at least once投递业务层幂等”是很经典的做法。这轮面试氛围明显轻松一些因为题目不是考察“你会不会”而是考察“你有没有真正处理过线上问题”。我当时没有在生产环境处理过高并发但之前在学校实验室里搭过一个简单的消息队列系统靠这个经验勉强应对下来了。现在回想这道题的底层设计思路在今天看来非常普遍但在2017年能把这套东西融入一道校招面试题说明猿辅导当时的技术团队是有实战沉淀的。工程岗面试不只看算法这道题帮我提前认识到了这一点。6. 教育业务题如何用产品思维回答技术问题第四面快结束的时候面试官问了一个让我印象非常深刻的问题“如果让你在猿辅导的App里设计一个功能帮助家长了解孩子的学习情况你会怎么做”这轮面试我印象最深的是面试官全程没有问任何一个具体的技术实现而是让我描述产品功能、数据展示和用户价值。我用了一个比较笨但有效的方式回答——我先问面试官“家长最关心什么”他没有直接回答而是反问我“你觉得呢”。于是我基于常识做了推测家长最关心的是孩子有没有在学习、学得怎么样、和其他孩子的差距在哪里。顺着这个推测我在白板上画了一个草图首页展示孩子今天的上课时长、练习题完成数、正确率和老师点评。每周发一封学习报告包含本周知识点掌握情况排名、薄弱点提示和下周学习建议。家长可以设置“学习目标”比如每天完成10道题系统按完成情况推送通知。面试官听完后说“你刚才这些功能里哪些你觉得技术上最难实现”我卡了一下然后说“知识点掌握情况的准确量化”应该是难度最高的因为它需要题目标签体系、能力值估计模型和大量的历史数据。他又追问“如果不用AI模型只用简单的统计方法你怎么量化”我回答可以用正确率加权的移动平均再结合做题数量做置信度调整。这道题让我意识到教育公司的技术面试到后期最重要的不是技术本身而是对教育这件事的理解——用户是谁、痛点是什么、数据如何反馈闭环。这是我整场面试里学到最多的一段比后面收到的offer更有价值。7. HR面与细节复盘那些容易被忽略但决定结果的小事下午场的最后一轮是HR面。这个环节看起来没什么技术含量但翻车概率并不低。我看到不少同学挂在HR轮不是能力不行而是沟通上的一些细节出了问题。我遇到的HR面试官问的问题比较常规自我介绍、为什么选择教育行业、对加班的看法、期望薪资。但有一个问题我觉得值得拿出来分享——她问“你最大的缺点是什么”。这个问题几乎是HR面的必考题很多人会答“我太追求完美”“我太拼命工作”这种回答一听就是假的。我当时回答的是“我比较慢热进入陌生环境后需要一段时间才能融入团队前几周可能显得比较沉默但熟悉之后会好很多。”这个回答的妙处在于它是个真实的短板但又不是影响核心工作的致命缺陷而且我顺带给出了改善的方式显示了自己有自我反省的能力。再补充几个容易被忽略的细节简历上写的每一个项目都必须能讲出技术选型的理由。面试官大概率会顺着简历往下问不会问你简历上没有的东西。如果简历上写了“使用Redis做缓存”那就要准备好回答“为什么不用本地内存缓存Redis的数据淘汰策略是什么”这类问题。面试过程中遇到不会的问题尽量先说思路。即使是错的也比直接说“不会”好很多。我当时在第三面被问到一个关于JVM内存模型的问题实际只知道大概概念我先正面回答了“运行时数据区分为堆、虚拟机栈、本地方法栈、方法区、程序计数器五个部分”然后主动说“方法区在JDK8里被移到了元空间”面试官没有再深入追问。用一句对的知识点把话题引向自己熟悉的方向是面试里很有用的技巧。时间上下午场整体节奏会比上午场稍微紧张一些。上午场通常有一个小时的笔试环节下午场可能直接进入面试需要全程保持高度集中。我当时在第二面结束后休息了十分钟喝了杯水简单回顾了一下自己前面答过的题目避免二面结束时遗留的问题影响三面心态。这个习惯后来我面试其他公司时也一直沿用效果很好。最后补充一下面试结束后的总结方法。当天晚上我回到宿舍把所有题目重新在白纸上复盘了一遍重点标记了那些被追问后没答好的点。这个习惯帮助我在后面的面试中明显减少了同类问题的失分也让我后来面试别人时更清楚该重点考察什么。
返回列表