ARTICLE DETAIL

资讯详情

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

猎豹移动研发工程师笔试题攻略:高频考点与实战解法

猎豹移动研发工程师笔试题攻略:高频考点与实战解法 猎豹移动的笔试通知下来那天我正在宿舍里刷排序算法。2016年那会儿移动工具类产品正处在出海最热闹的阶段猎豹清理大师在海外市场一路狂奔研发岗的简历投递量相当可观。很多人以为做安全清理软件的公司笔试题肯定要考病毒分析、恶意代码识别结果卷子真正发下来大家才发现想多了从选择题到编程题清一色是计算机基础数据结构、算法、网络、操作系统轮番上阵。这篇文章不是官方真题集而是基于那几年猎豹移动研发工程师笔试的题型风格、多位候选人复盘整理出的一套拆解思路覆盖高频考点与实战解法给准备客户端、后端研发岗位笔试的朋友做个参考。1. 笔试的整体格局猎豹移动2016年那套卷子考什么1.1 研发工程师的能力画像猎豹移动2016年的研发工程师岗位招人方向大致集中在Android客户端、C底层引擎、服务端后端这几个序列。核心产品Clean Master本身是C做底层清理引擎、Java做Android应用层所以笔试并不只盯着一门语言而是在考察一个工程师的综合计算机素养。和算法岗不同研发工程师笔试的重点不是比谁刷题刷得深而是看候选人有没有完整、扎实的计算机知识体系。比如C岗位会涉及内存管理和多线程Java岗位会涉及集合类和并发包但无论哪个方向算法与数据结构都是必考大头。从当年的情况看笔试通过率并不高刷掉的人绝大多数不是不会编程而是基础概念上的漏洞太多。1.2 题型分布与时间分配综合多位参加过笔试的同学反馈卷子结构大致可以归纳为三类选择题、简答题、编程题。选择题通常在25到30道之间覆盖语言基础、数据结构、网络、操作系统、Linux命令简答题一般有两三道偏向场景分析和设计思路编程题则有二到三道全部是核心算法题。整套卷子的完成时间是90到120分钟总分100到120分。不少人在编程题上耗时过多前面选择题没时间检查丢了很多不该丢的分。我的建议是时间分配直接按40%给选择题、20%给简答题、40%给编程题来切先保证把会做的题稳定拿分再考虑挑战难题。1.3 难度定位没有偏题怪题拼的是基础扎实度这套卷子整体难度中等偏上但并不是靠冷门题目来为难人。它考察的知识点全部来自本科课程的经典范畴甚至很多题一眼看去会觉得这我学过。真正的区分度恰恰藏在基本功的细节里二分查找的边界条件怎么写、TCP的TIME_WAIT状态什么时候出现、虚函数析构为什么要声明为virtual这些点平时不较真考场上就很容易翻车。2. 选择题中那些藏着陷阱的基础知识2.1 C与Java语言层面的易错点C选择题在猎豹这套笔试里占比不小考点集中在构造函数与析构函数执行顺序、虚函数与多态、拷贝构造函数何时被调用、static关键字的作用、const与指针结合时的语义以及动态内存管理的相关问题。举个例子看下面这段代码#include iostream using namespace std; class Base { public: Base() { cout Base构造 endl; } ~Base() { cout Base析构 endl; } }; class Derived : public Base { public: Derived() { cout Derived构造 endl; } ~Derived() { cout Derived析构 endl; } }; int main() { Base* p new Derived(); delete p; return 0; }如果面试者答错十有八九是卡在析构顺序上。new Derived()会先执行Base的构造函数再执行Derived的构造函数这是大多数人知道的。但delete p这里隐藏着一个坑如果Base的析构函数没有声明为virtual那么通过Base指针delete Derived对象时只会调用Base的析构函数Derived的析构函数根本不会执行内存释放就不完整。正确做法是把Base析构函数声明为virtual ~Base()。这类题在卷子里出现频率很高本质上是在考察你有没有真正理解C对象模型中的虚函数表机制。Java方向的选题则更偏向集合类与并发包底层。HashMap的put流程、为什么String是不可变的、ArrayList与LinkedList的增删查改复杂度差异、ConcurrentHashMap的分段锁机制都是高频考点。那几年的Java题目还特别喜欢问HashMap在并发场景下为什么可能形成循环链表这牵涉到JDK 1.7头插法在扩容时出现的问题也要求你知道JDK 1.8改尾插法之后这个隐患被消除。2.2 数据结构与算法选择题的常见考法数据结构这块选择题最爱考的是复杂度计算、栈与队列的应用场景、排序算法的稳定性与比较次数、二叉树的性质以及哈希冲突的处理方式。有一类经典题几乎是每场笔试必出给一棵完全二叉树已知节点总数为N求叶子节点数、度为1的节点数、度为2的节点数。这类题的关键是记住任意二叉树中叶子节点数等于度为2的节点数加1也就是N0 N2 1。结合完全二叉树的性质度为1的节点数至多只有1个就能很快解出来。排序算法也是出题大户但考的不是让你默写代码而是比较不同排序算法在最坏情况下的时间复杂度、稳定性以及数据规模对选型的影响。快速排序平均复杂度是O(n log n)但最坏情况是O(n²)归并排序永远稳定在O(n log n)但需要额外O(n)空间堆排序空间复杂度是O(1)但实际工程中因为缓存不友好排序速度往往不如快排。这些判断题没有难度纯粹看你对概念的记忆清晰不清晰。2.3 操作系统与计算机网络的高频考点操作系统选择题喜欢从这几个角度切入进程与线程的区别、死锁的四个必要条件、虚拟内存与页面置换算法、进程间通信方式。考死锁时循环等待、互斥、不可剥夺、请求保持这四个条件缺一不可题目有时会故意去掉一个条件问以下哪种情况下不会发生死锁其实就是在考有没有把概念记全。计算机网络部分的考点则相当集中TCP三次握手与四次挥手、TCP与UDP的区别、滑动窗口与拥塞控制、HTTP状态码语义、DNS解析过程。猎豹做的是客户端产品对网络传输这块尤其看重因为清理工具要频繁与后端服务器交互。TIME_WAIT状态是常客主动关闭连接的一方在收到对端FIN后进入TIME_WAIT等待2MSL才真正关闭。为什么必须等待2MSL因为要确保最后一个ACK能够到达对端同时让旧连接的延迟数据包在网络中彻底消失。这个知识点光背结论不够要理解背后的理由因为简答题也可能问你类似的问题。3. 算法编程题从题目描述到完整解法3.1 第一类题字符串处理基本功的试金石字符串处理题几乎出现在每一年的笔试里猎豹2016年也不例外。它考察的不是高深算法而是代码的严谨性以及对边界条件的敏感度。印象比较深的一道题是给定一个英文句子要求反转单词顺序但是不借用额外空间不能使用辅助数组。例如输入the sky is blue输出blue is sky the。如果允许额外空间解法很简单按空格切分后逆序拼接。但题目限制不能使用额外空间这就需要用经典的两步翻转法#include iostream #include string #include algorithm using namespace std; void reverseWords(string s) { // 整体翻转 reverse(s.begin(), s.end()); int n s.size(); int start 0; for (int i 0; i n; i) { if (i n || s[i] ) { // 遇到空格或字符串末尾翻转当前单词 reverse(s.begin() start, s.begin() i); start i 1; } } } int main() { string s the sky is blue; reverseWords(s); cout s endl; return 0; }第一步把整个字符串翻转成eulb si yks eht第二步对每个单词内部再做一次翻转就得到blue is sky the。整体时间复杂度O(n)空间复杂度O(1)完全满足题目限制。这类题务必注意循环条件里要处理i n的情况否则最后一个单词会在翻转时被漏掉。这种细节丢了分真的非常可惜。字符串题还有一个很常见的就是实现简单的atoi函数。看起来简单实际上要处理的边界条件很多跳过前导空格、处理正负号、判断溢出、忽略非法字符。当年有不少人在这道题上栽跟头不是思路问题而是没意识到溢出判断需要考虑INT_MAX和INT_MIN。3.2 第二类题动态规划考察状态定义能力动态规划在猎豹笔试中几乎是必考的。2016年常见的动态规划题有最大连续子数组和、最长公共子序列、编辑距离、背包问题变体。动态规划的难点不在代码实现而在状态定义和转移方程的推导。以最长公共子序列为例这是一道非常经典的题目。设两个字符串分别为A和B状态dp[i][j]表示A的前i个字符与B的前j个字符的最长公共子序列长度。转移方程如下如果A[i-1] B[j-1]则dp[i][j] dp[i-1][j-1] 1否则dp[i][j] max(dp[i-1][j], dp[i][j-1])#include iostream #include string #include vector #include algorithm using namespace std; int longestCommonSubsequence(string a, string b) { int m a.size(), n b.size(); vectorvectorint dp(m 1, vectorint(n 1, 0)); for (int i 1; i m; i) { for (int j 1; j n; j) { if (a[i - 1] b[j - 1]) { dp[i][j] dp[i - 1][j - 1] 1; } else { dp[i][j] max(dp[i - 1][j], dp[i][j - 1]); } } } return dp[m][n]; } int main() { string a abcde; string b ace; cout longestCommonSubsequence(a, b) endl; return 0; }LCS的时间复杂度是O(mn)空间复杂度是O(mn)。如果题目要求空间优化可以用滚动数组把空间降到O(min(m,n))因为dp[i][j]只依赖前一行和当前行的数据。答题思路分三步先定义状态再写转移方程最后确定边界条件。有些同学一上来就写代码状态没定义清楚写到一半发现逻辑对不上白白浪费大量时间。我的经验是花两分钟在草稿纸上把状态和转移方程写出来再动手码代码效率反而更高。3.3 第三类题二叉树遍历从递归到非递归的转变二叉树相关的编程题在2016年的笔试里也很常见。层序遍历、求二叉树最大深度、最近公共祖先LCA都是高频原型。以层序遍历为例思路是用队列进行广度优先搜索每遍历完一层就把结果存下来。关键点在于如何判断当前层是否结束。常见做法是在每一轮循环开始时记录当前队列的大小size然后循环size次恰好把当前层的所有节点处理完再进入下一轮。#include iostream #include vector #include queue using namespace std; struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; vectorvectorint levelOrder(TreeNode* root) { vectorvectorint result; if (root nullptr) return result; queueTreeNode* q; q.push(root); while (!q.empty()) { int size q.size(); vectorint level; for (int i 0; i size; 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 现场编程的答题技巧与踩坑点在牛客网或者赛码网这类平台上做笔试很多人不是不会解题而是被输入输出格式卡住了。2016年的笔试环境不算友好有些人甚至因为读入数据的代码没写对导致本地测试通过但线上始终AC不了。最基本的教训是先确认题目要求的输入格式是单组还是多组。多组数据时在C里通常要用while (cin n)这种循环读取方式Java则用while (scanner.hasNext())。忘记写循环读取只处理了第一组数据系统就会判定你的代码不通过。边界条件也要重视。数组长度为0、节点为空、字符串全部是空格这些极端情况都要在编码时提前考虑。编程题里有个约定俗成的原则第一件事就是检查输入是否合法如果输入非法直接返回空值或0不要贸然继续处理。这不是怂而是避免在空指针、数组越界这些低级错误上白白扣分。4. 编程语言与Linux环境题贴近真实开发场景的考点4.1 内存管理与指针问题客户端岗位的必修课猎豹移动的底层引擎大多用C开发内存管理相关的简答题在笔试中相当常见。典型问题包括new和malloc的区别是什么如何检测内存泄漏什么是悬空指针new和malloc的区别是个老生常谈的问题但很多人答不全。malloc是C标准库函数只负责分配指定字节数的内存返回void*不调用构造函数new是C运算符分配内存的同时会调用对象的构造函数返回的类型是具体类型的指针。对应的free只释放内存不调用析构函数delete会先调用析构函数再释放内存。更深一层的考点是new[]和delete[]必须配对使用否则可能导致内存泄漏或者未定义行为。猎人公司做底层引擎时对这类细节要求很严岗位不同但基础知识的考核标准是一致的。悬空指针的问题则更贴近实战。指针指向的内存被释放后指针本身还保留着原来的地址值如果再对这样的指针做解引用操作轻则读到垃圾数据重则导致程序崩溃。规避方法是在delete之后把指针置为nullptr并在删除容器元素时注意迭代器失效问题。这种思路在笔试中未必直接考代码但会以场景题的形式出现一段程序偶尔崩溃让你分析可能的原因如果能走悬空指针-越界访问-栈溢出这条排查链路得分完整度会好很多。4.2 Linux命令行与调试工具拉分项就在这里研发工程师日常打交道最多的就是Linux服务器笔试中也会穿插一些Linux相关的题目。高频考点集中在文件操作命令、文本处理命令、进程与端口排查、日志分析以及gdb的基本用法。常见题目场景服务器上某个进程CPU占用率特别高如何定位问题排查步骤一般是这样先用top查看系统整体负载找到占用CPU最高的进程PID再用ps -Lp PID -o pid,tid,pcpu,comm查看该进程内各线程的CPU占用定位具体是哪个线程的问题用top -H -p PID进入线程视图进一步确认可疑线程ID如果进程是Java应用用jstack PID把线程栈打印出来比对刚才记录的线程ID十六进制转换找到对应的业务代码位置如果是C进程则在gdb里执行attach PID用thread和bt命令查看线程栈这类题目考的不是你要背下来所有参数而是你有没有真正用这些命令解决过线上问题。卷子上那些通过率最低的简答题通常就是这种带有生产环境味道的题目。平时多在实际环境中跑一跑命令比考前临时背命令参数要有效得多。4.3 多线程与并发问题通信与同步缺一不可多线程题目在笔试中出现频率很高。经常被问到的问题是如何实现一个线程安全的生产者消费者模型请用C或Java描述思路。解答这类题有两个层面。第一层是思路生产者往队列里放数据消费者从队列里取数据队列满时生产者等待队列空时消费者等待需要通过互斥锁保护队列通过条件变量实现等待与唤醒。第二层是代码用std::mutex配合std::condition_variable或者Java的synchronized配合wait()与notifyAll()。一个简化的C回答版本如下#include iostream #include mutex #include condition_variable #include queue #include thread using namespace std; template typename T class BlockingQueue { public: explicit BlockingQueue(size_t capacity) : capacity_(capacity) {} void put(const T item) { unique_lockmutex lock(mutex_); not_full_.wait(lock, [this]() { return queue_.size() capacity_; }); queue_.push(item); not_empty_.notify_one(); } T take() { unique_lockmutex lock(mutex_); not_empty_.wait(lock, [this]() { return !queue_.empty(); }); T item queue_.front(); queue_.pop(); not_full_.notify_one(); return item; } private: mutex mutex_; condition_variable not_full_; condition_variable not_empty_; size_t capacity_; queueT queue_; };这里有几个容易被忽视的细节条件变量的wait一定要在循环中使用因为可能发生虚假唤醒wait会临时释放互斥锁让其他线程有机会进入临界区notify_one和notify_all的使用场景不同单生产者单消费者用notify_one就够了多消费者场景需要考虑是否正确唤醒。笔试中的多线程题不一定要求写完整代码但至少要能说清楚锁与条件变量各解决了什么问题。锁保证的是共享数据访问的互斥性条件变量解决的是线程间的同步等待与通知两者配合协作才能实现线程安全的阻塞队列。如果只提到锁没提条件变量回答就缺了一角。5. 实战复盘我从这套卷子里总结出的备考方法5.1 优先级排序把有限时间花在刀刃上回看猎豹这套卷子整体的备考优先级应该是算法与数据结构大于语言基础语言基础大于网络与操作系统网络与操作系统大于工具经验。这个排序不是我拍脑袋想出来的而是根据笔试的分值分布推导出来的。算法题每道20到30分选择题里的数据结构相关题也有五六道语言基础题集中在C或Java选择题与简答题网络和操作系统通常只占选择题的个位数。如果没有太多时间准备先抓算法再抓语言重点这个顺序大概率不会错。5.2 错题本的正确使用方式很多同学也做错题本但做成了抄题机器题目抄了一遍答案抄了一遍之后再也不看。这样的错题本毫无意义。正确的做法是每道错题按考点归类标注错误原因是概念理解错误、边界条件遗漏还是纯粹细节疏忽。每周固定时间把本周错题重新做一遍注意是重新做不是看一遍。看得懂答案和能独立写出来完全是两码事笔试要的是后者。5.3 考前两周的模拟训练方案考前两周是黄金冲刺期我建议按下面这个节奏来准备。每天固定完成一套限时模拟卷最好用和真实考试一样的在线OJ系统模拟真实环境下的代码输入输出处理。每套题做完之后不是对完答案就结束要花至少半小时复盘错在哪一步、卡在哪一秒、时间分配是否合理、下次如何调整。手写代码的训练同样重要。笔试平台上代码补全和自动提示会掩盖一些问题而面试的现场手写代码环节没有这些辅助。每天挑两道经典算法题要求自己在纸上或编辑器无自动补全的环境下写出来才能检验真正的编程熟练度。最后一天不要再刷新题把过去两周做错的题目全部翻一遍形成整体的记忆回扫。我自己考完这套笔试题后最大的感受是笔试不是智力竞赛而是基本功的体检。考场上遇到不会的题不要慌着崩溃先把题目中的已知条件写下来尝试寻找和已做过的题型的关联很多时候答案就在藏在你最熟悉的知识点里。把心态放稳把该拿的分拿住结果不会差。
返回列表