猎豹移动研发工程师笔试复盘:链表、二叉树、TCP与C++考点

猎豹移动研发工程师笔试复盘:链表、二叉树、TCP与C++考点 猎豹移动这个名字放在2016年绝对是移动互联网圈躲不开的存在。Clean Master、猎豹清理大师那一票工具产品全球装机量过亿研发团队扩张速度快得吓人校招和社招的笔试题自然也是硬桥硬马很少玩虚的。那时候你投一份研发工程师简历过去大概率会先收到一封在线笔试链接的邮件90分钟题量不小客观题加编程题混着来时间紧到让你没空纠结“要不要先做难题”。这份笔试题虽然已经是几年前的旧题了但对现在准备互联网公司研发岗的同学来说参考价值一点都没过时。原因很简单移动端研发考察的核心知识点链表、二叉树、进程线程、TCP、C内存管理这些内容到今天依然是笔试常客只是换了个马甲而已。这篇文章我会把当年猎豹移动研发工程师笔试的高频考点、题型分布、典型题目和参考解法完整拆一遍顺便聊聊站在面试官角度这类笔试到底在筛选什么人。1. 当年的招聘背景与笔试题型分布1.1 猎豹移动的黄金年代与研发岗画像2016年的猎豹移动正处于“工具出海”故事讲得最漂亮的阶段。母公司金山系背景加上Clean Master在海外的疯狂增长让猎豹的技术团队有一种很明显的基因重底层、重性能、重系统级优化。这类业务形态决定了它招研发工程师时对操作系统原理和C/C功底的要求明显高于普通业务型互联网公司。所以当年猎豹移动的笔试题目不会像部分纯业务公司那样大量考察框架使用、项目经验堆砌而是集中在计算机基础、数据结构和算法。Android开发岗还要额外考Java和Android生命周期C方向则会加大内存管理、指针、STL底层实现的比重。整体感觉就是“基础不牢笔试基本陪跑”。1.2 笔试题型分布客观题、编程题与设计题的比例从我收集到的候选人反馈来看猎豹移动2016年研发工程师笔试大致是这样一个结构90分钟20到25道选择题部分为多选2到3道编程题偶尔带一道系统设计题。客观题覆盖数据结构、操作系统、计算机网络、C/C或Java语言特性编程题以链表、二叉树、字符串处理为主。我画了一下大致比例方便你感受当时题型的重心题型占比高频考点选择题50% - 60%数据结构、操作系统、网络协议、语言特性编程题30% - 40%链表操作、二叉树遍历、动态规划、字符串设计/简答题0% - 10%内存管理方案、并发设计、开放场景题移动端研发尤其喜欢考链表和指针操作因为App里大量涉及内存受限下的数据管理链表的增删改查写不写得利索基本能看出一个工程师对内存操作的熟练度。我当时给准备笔试的学弟学妹的建议是剑指Offer刷两遍是底线高频题要练到闭着眼睛能写出来为止。2. 核心考点拆解数据结构与算法2.1 链表与指针操作——笔试中的“必考送分题”链表是猎豹移动笔试选择题和编程题双重高频的考点。选择题喜欢考“在单链表中删除一个节点的时间复杂度”“判断链表是否有环的空间复杂度”编程题则经常直接让你手写反转链表、合并有序链表、删除倒数第N个节点。这类题目之所以被反复拿出来考不是因为它难而是因为它能一次性暴露三个问题第一会不会用指针或引用操作节点第二边界条件考虑是否周全比如空链表、单节点链表第三代码风格是否干净能不能让阅卷官一眼看明白逻辑。我当年笔试前特意把链表系列题全部手写了一遍包括带头节点和不带头节点的反转受益匪浅。一个典型的反转链表实现用C写大概是这样的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* nextTemp curr-next; curr-next prev; prev curr; curr nextTemp; } return prev; }注意这里的命名和逻辑每轮循环先保存当前节点的下一个节点再翻转指针方向。漏掉“先保存next”这一步链表就断了这也是很多新手最容易翻车的地方。答题时把注释写清楚哪怕代码有个别小bug阅卷官也容易给步骤分。2.2 二叉树遍历与递归——考察“基本功”的试金石二叉树遍历在当年的笔试里几乎是出场率最高的算法题。选择题常考“已知先序和中序求后序”编程题则喜欢考层次遍历、二叉树最大深度、判断平衡二叉树。这些题目考的不是多高深的算法而是递归思维的熟练度——能不能在几分钟内把递归结构写对直接反映候选人平时写代码的量够不够。以层次遍历为例核心思路是用队列做广度优先搜索按层输出节点值vectorvectorint levelOrder(TreeNode* root) { vectorvectorint result; if (root nullptr) return result; queueTreeNode* q; q.push(root); while (!q.empty()) { int levelSize q.size(); vectorint currentLevel; for (int i 0; i levelSize; i) { TreeNode* node q.front(); q.pop(); currentLevel.push_back(node-val); if (node-left ! nullptr) q.push(node-left); if (node-right ! nullptr) q.push(node-right); } result.push_back(currentLevel); } return result; }这里有个细节进入每一层之前先记录当前队列的长度levelSize然后用一层循环处理这一层的所有节点。如果不先记录size直接把q.size()放在for循环条件里队列是会动态变化的输出就会完全混乱。这种细节恰恰是笔试阅卷时区分“真会”和“背过”的关键。2.3 动态规划与贪心——拉开差距的关键题型猎豹移动的笔试里动态规划题目一般不会直接告诉你“这是一道DP题”而是穿上一层字符串或数组的外衣。比如最长无重复子串、编辑距离、最长公共子序列这些都是当年出现频率较高的题目。这类题目的难点在于状态转移方程能不能想清楚想明白以后代码量其实很小。以“最长无重复字符的子串”为例最直观的解法是用滑动窗口加哈希表维护窗口内字符的状态int lengthOfLongestSubstring(string s) { unordered_mapchar, int charIndexMap; int maxLen 0; int left 0; for (int right 0; right s.length(); right) { char c s[right]; if (charIndexMap.find(c) ! charIndexMap.end() charIndexMap[c] left) { left charIndexMap[c] 1; } charIndexMap[c] right; maxLen max(maxLen, right - left 1); } return maxLen; }这里有一个很容易忽略的坑在更新left指针时要判断之前出现过的字符位置是否在当前窗口内也就是说哈希表中记录的旧位置必须大于等于left才需要更新。否则窗口外已经废弃的字符位置会把left拉回去导致结果错误。笔试时间紧张的时候这种细节最容易错所以我建议平时做题就养成在关键判断处写注释的习惯。提示面试官看代码时不一定逐一编译运行。但他们会重点看你的状态转移条件、边界判断和返回值逻辑是否合理。把注释写清楚很大程度上能弥补代码工整度的不足。3. 操作系统与网络基础——容易被忽视的丢分区3.1 进程线程与死锁——经典理论题怎么答操作系统在猎豹移动笔试选择题里占的比重不小尤其集中在进程与线程、死锁、内存管理这几个方向。这类题目对长期写业务代码的候选人来说反而容易丢分因为平时不太接触全靠考前突击。进程和线程的区别选择题常考的角度包括哪个资源是进程独享的、哪个是线程共享的进程间通信方式有哪些线程间同步手段有哪些。死锁则是另一种常客四个必要条件互斥、持有并等待、不可剥夺、循环等待几乎是必背内容。笔试喜欢给出一个场景让你判断“当前系统是否可能发生死锁”或者“破坏哪个条件可以避免死锁”。应对这类题的策略很简单把《现代操作系统》或考研教材里对应的章节过一遍然后把常见考点整理成一句话笔记。比如信号量和互斥锁的区别核心就一句“互斥锁只能用于保护临界区信号量还能用于同步”。这种高度浓缩的结论在选择题阶段非常好用。3.2 TCP/IP与HTTP——移动端研发绕不开的协议细节网络部分的考察重点集中在TCP三次握手、四次挥手、滑动窗口、TCP和UDP的区别、HTTP与HTTPS的区别。移动端研发考察网络协议背后逻辑很现实App请求要经过复杂的弱网环境团队需要工程师真正理解连接建立、数据可靠传输和断开连接的过程否则线上问题排查无从下手。TCP三次握手的“为什么是三次而不是两次”是当年笔试的高频讨论点。常见的标准回答是三次握手能确保双方都具备收发能力并能同步初始序列号。更深入的回答会提到防历史重复连接初始化如果只有两次握手服务器无法确认客户端是否收到了自己的SYNACK在超时重传场景下可能建立多余连接。这种层次分明的回答在简答题里会明显加分。HTTP和HTTPS的区别也经常考。注意答题思路不能停留在“HTTPS比HTTP多了一层SSL/TLS”要补充说明TLS握手的核心步骤交换证书、协商密钥、加密传输以及为什么这样能防止中间人攻击。这些细节在选择题里可能以“哪个协议用于协商对称加密密钥”的方式出现。4. 编程语言与综合能力题——C/Java与设计思维4.1 C内存管理与Java GC——语言题的重灾区猎豹移动笔试中C方向的候选人会遇到大量内存管理题目野指针、内存泄漏、栈和堆的区别、new/delete与malloc/free的差异、智能指针的使用场景。选择题经常给出类似“char* p new char[100]; delete p; 会发生什么”的题目。答案是未定义行为因为new[]必须配对delete[]否则可能只调用第一个元素的析构函数数组越界部分不会被正确释放。Java方向则喜欢考JVM内存结构、GC Root、垃圾回收算法、HashMap底层原理。HashMap在1.7和1.8版本中的区别头插法 vs 尾插法、红黑树引入条件也是高频考点。这些题目对于常年用框架写CRUD的人来说可能偏底层但恰恰是这些底层原理决定了你在处理线上OOM、频繁Full GC问题时能不能快速定位。我个人的备考建议是C候选人把《Effective C》里关于资源管理的章节刷两遍Java候选人重点看《深入理解Java虚拟机》的内存管理和垃圾回收章节。不需要背整本书但高频概念要能用自己的话说明白。4.2 设计题与场景题——考察工程思维的开放题猎豹移动当年笔试偶尔会带一道设计题比如“设计一个LRU Cache”“设计一个线程安全的单例模式”“如何设计一个接口限流方案”。这类题目没有标准答案阅卷官重点看两点思路是否结构化、是否考虑了边界条件和并发安全。以“设计线程安全的单例模式”为例不少候选人第一反应是写双重检查锁。但真要拿高分还需要主动说明volatile关键字的作用防止指令重排导致拿到未初始化完成的对象。如果顺手补充一句“除了双重检查锁还可以用静态内部类方式实现由JVM保证类加载的线程安全”这道题的区分度立刻就拉开了。设计LRU Cache则需要说明用哈希表加双向链表的原因哈希表保证O(1)查找双向链表保证O(1)插入和删除两种数据结构组合才能满足所有操作的时间复杂度要求。能把这个组合逻辑讲清楚比背下代码更重要。5. 笔试题实战复盘一套还原版模拟卷5.1 客观题实录与解析下面这套模拟题是我根据当年猎豹移动笔试风格整理的覆盖了大部分高频考点。每道题后面附了解析方便你自查。选择题第1题在单链表中已知指向某个节点的指针p要在p之后插入一个新节点时间复杂度是多少A. O(1)B. O(n)C. O(logn)D. O(n²)解析在已知节点后插入只需要修改两个指针时间复杂度O(1)。本题考察“已知前驱节点”和“不知道前驱节点”两种情况下的操作复杂度差异。如果是删除已知节点p且只有单链表则需要遍历找到p的前驱复杂度O(n)。选择题第2题下列哪种方式不能用于进程间通信A. 管道B. 消息队列C. 共享内存D. 临界区解析答案是D。临界区是进程内线程间同步的手段不是进程间通信手段。注意区分同步和通信两个概念。选择题第3题关于TCP三次握手下列说法正确的是A. 客户端收到服务器的SYNACK后连接就建立了B. 第三次握手可以携带数据C. 两次握手也能完全避免重复连接D. 服务器收到ACK后进入SYN_RCVD状态解析答案是B。第三次握手客户端已确认服务器收发正常可以携带数据。选项A不正确因为服务器端的连接要收到第三次握手的ACK后才真正建立。选项C不正确两次握手无法防止历史重复连接的初始化。填空题第4题在一个长度为n的数组中找出出现次数超过n/2的数字要求时间复杂度O(n)、空间复杂度O(1)可以使用________算法。解析摩尔投票法。核心思路是维护候选众数和计数器遍历完数组后再次验证候选者是否真的大于n/2。这道题特别容易漏掉二次验证笔试时一定要记得写。选择题第5题关于HashMap在Java 8中说法错误的是A. 链表长度超过阈值时链表会转为红黑树B. 查找时间复杂度最坏为O(logn)C. 扩容时元素会重新计算哈希D. 线程不安全但可以用Collections.synchronizedMap包装解析答案是C。Java 8中扩容时对元素位置做了优化不需要全部重新计算哈希而是通过原哈希值高位与新增位的关系将链表拆分为“原位置”和“原位置旧容量”两个链表。这是一个非常经典的优化点。5.2 编程题实录与参考思路编程题1给定两个有序链表的头节点将它们合并为一个新的有序链表并返回。思路用哨兵节点简化边界处理用双指针依次比较两个链表当前节点的大小将较小节点接到结果链表尾部。当其中一个链表遍历完时直接把另一个链表剩余部分接到尾部。时间复杂度O(nm)空间复杂度O(1)。ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* tail dummy; while (l1 ! nullptr l2 ! nullptr) { if (l1-val l2-val) { tail-next l1; l1 l1-next; } else { tail-next l2; l2 l2-next; } tail tail-next; } if (l1 ! nullptr) tail-next l1; if (l2 ! nullptr) tail-next l2; return dummy.next; }这里用了一个本地哨兵节点dummy避免处理头节点为空和确定新头节点的繁杂逻辑。这个技巧在链表面试题里非常实用能显著降低代码出错概率。编程题2给定一个字符串请你找出其中不含有重复字符的最长子串的长度。思路滑动窗口。维护窗口的左右边界和一个哈希表记录每个字符最近一次出现的位置。右指针不断向右扩展遇到重复字符时将左指针移动到该字符上次出现位置的下一位。整个过程只需要遍历一遍字符串时间复杂度O(n)。int lengthOfLongestSubstring(string s) { vectorint lastIndex(256, -1); int left 0; int maxLen 0; for (int right 0; right s.length(); right) { char c s[right]; if (lastIndex[c] left) { left lastIndex[c] 1; } lastIndex[c] right; maxLen max(maxLen, right - left 1); } return maxLen; }我给的版本用vector模拟哈希表因为字符集固定为256这样比unordered_map更快也不涉及哈希函数的开销。笔试时如果能用数组代替哈希表通常能体现你对题目细节的思考。编程题3给定一个二叉树返回其节点值的层次遍历结果从底向上按层输出。思路先做常规的从上到下层次遍历得到每一层的节点列表最后整体反转结果数组即可。也可以用链表头插法但代码可读性会差一些。6. 面试官视角的评分逻辑与备考建议6.1 阅卷时最看重什么——笔试的隐性评分标准作为经历过阅卷的人我可以说一句实话笔试阅卷不追求完美答案而是快速筛选候选人的基础能力和思维习惯。选择题看正确率编程题看思路是否清晰、边界是否考虑完整、代码是否规整设计题则看能否给出结构化方案并兼顾并发与扩展。编程题最让人头疼的几种情况注释一堆但主逻辑没实现代码能跑通但完全没考虑空输入和边界输入函数名和变量名随意用a、b、c逻辑晦涩难懂。这三种情况都会直接影响阅卷官对候选人的印象。反过来如果你能在代码开头用一两句话说明解题思路在关键判断处写清楚注释即使代码有轻微瑕疵也容易拿到步骤分。时间分配上我建议客观题控制在60分钟内完成留30分钟给编程题。编程题不要一上来就写代码先在草稿纸上画一下思路确认边界条件后再动手。如果一道题卡了10分钟还没有头绪果断跳过做下一道不要因小失大。6.2 不同基础候选人的备战清单针对不同基础的候选人列一份实用的笔试备战清单按优先级排序优先级项目说明P0剑指Offer刷两遍覆盖绝大多数笔试常考的数据结构与算法题型P0LeetCode高频题分类刷链表、二叉树、字符串、动态规划四个分类至少各刷20题P1操作系统核心概念进程线程、死锁、内存管理、文件系统配套做选择题P1计算机网络核心协议TCP三次握手/四次挥手、HTTP/HTTPS、DNS解析过程P2语言底层原理C内存管理、STL底层Java JVM、GC、HashMapP2项目复盘准备一个能讲清楚技术细节和踩坑经历的项目时间有限的话P0项决定你能不能过笔试P1项决定你能不能拿到中等偏上的分数P2项是冲刺高分与进入面试后的加分项。很多同学会陷进一个误区花大量时间刷难题偏题结果基础小题反而丢分。实际上这类笔试的客观题占了半壁江山客观题的正确率是确保笔试通过的基本盘。我个人还有一个建议不要只刷题不总结。每做错一道题把错因归类到“知识点缺失、边界条件遗漏、审题不仔细、代码实现错误”四个类别里。一周后回看这些错因你会发现薄弱点非常集中针对性补起来效率比一遍遍刷题高得多。作为经历过那个时期笔试面试的技术人我的体会是猎豹移动这类公司的笔试题目真正筛选的并不是谁背的题多而是谁的基本功扎实。数据结构、操作系统、网络原理这些内容面试前突击三个月确实可以应付笔试但进了公司以后写代码才发现当年笔试里的每一道题都是在替工程实践的某些坑提前把关。最后再分享一个小技巧笔试前一周把链表反转、二叉树遍历、字符串滑动窗口这三类高频题各手写三遍直到闭着眼睛都能写出来。这个动作看起来简单但到了考场上能帮你节约大量思考时间把所有精力留给真正的难题。