尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

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

阿里2016研发笔试真题解析:Java/C++、算法与操作系统全考点拆解

阿里2016研发笔试真题解析:Java/C++、算法与操作系统全考点拆解 阿里2016研发工程师笔试在当年校招圈算得上是一套“风向标”级别的题。我到现在都记得那一年好多同学刷完这套题之后对“大厂考什么”这件事才算真正有了概念。它没有太多偏题怪题反而把大量篇幅集中在Java/C基础、算法与数据结构、操作系统、网络这些计算机基本功上考察方式也很典型选择题考理解编程题考手写代码。这一篇我把它拆开讲透结合我自己备考和后来实际工作的体会把每一类考点背后的答题逻辑、易错点和复习方法都聊一遍适合正在准备大厂研发岗笔试的应届生也适合想系统巩固基础的职场新人。1. 这套笔试题的背景与定位1.1 2016年阿里校招笔试的考察逻辑先说说这套题的整体感觉。整套卷子题量不算小选择题占了很大比例覆盖范围横跨Java、C、数据结构、算法、操作系统、计算机网络偶尔还会出现一两道数据库和设计模式的选择题。编程题通常是两道左右侧重链表、数组、字符串这类基础算法基本不会刻意出那种需要冷门数学技巧的题目。我当年做完的感受是题目本身不偏拼的是基础扎不扎实。这里有个很关键的信号大厂校招笔试本质上是海选目标不是要你考满分而是要把“基础不牢靠”的人筛掉。所以题目设计上会有大量“改一个条件就变成错误选项”的坑比如HashMap线程安不安全、volatile能不能保证原子性、快排最坏时间复杂度是多少这种看似简单但很容易想当然的问题。而这种坑恰恰是最能区分“背过”和“真懂”的地方。为什么这么说因为选择题如果只靠记结论选项稍一变型就容易翻车。举个例子有一类经典题是问“下列哪个说法是错误的”四个选项里可能分别涉及JVM内存分区、GC算法、类加载过程、线程状态每个选项都能单独展开成一道简答题。如果你对其中任意一个概念停留在“好像听说过”的程度就极大可能掉进出题人设置的混淆项里。而我后来工作之后回头看发现这套笔试的题干描述方式和平时做需求时接收到的描述特别像信息可能有点绕但关键约束都在条件里。能不能把题干里的约束提取出来直接决定了后面代码写不写得对。这种能力笔试在考工作也在考。1.2 从笔试看研发工程师的能力模型如果给这套题画一个能力模型图大概是三层第一层是语言与工具基础包括Java和C的语法细节、常用类库、内存模型第二层是通用计算机基础操作系统、网络、数据结构、算法第三层是工程素养包括代码书写规范、边界情况处理、时间复杂度分析。这三层不是独立考察的而是穿插在卷子里。我特别想强调工程素养这一层。很多人觉得笔试就是拼算法但实际上阿里这套题里有很多“代码细节”题考察的是你平时有没有正经写过代码。比如C题目里问析构函数为什么必须写成虚函数Java题目里问字符串拼接用和StringBuilder的区别这些东西不自己敲上几万行代码光靠背面试题是答不好的。还有一个容易被忽略的点就是手写代码的规范程度。笔试题往往是给你一个题目让你在一个限定的空白区域手写代码。注意这里的代码不是只要“能跑”就完事函数命名、返回值类型、异常处理、空指针判断都会影响得分。我当年做笔试时就因为在链表中忘记处理头节点为空的情况被扣过不少分这种教训从笔试一直延续到了工作中。所以这套题表面上是在考“你懂不懂”实际上是在考“你有没有工程实践的习惯”。如果你想把这套题真正吃透不能只看题解还得动手把每道编程题完整写一遍最好再想一想“如果输入是空的怎么办”“如果数据量特别大怎么办”。这些思考才是笔试真正想看到的。2. Java与C语言基础题的高频雷区2.1 Java方向集合、并发与JVMJava在2016年阿里研发笔试题里出现的频率非常高毕竟当时整个技术栈大量基于Java。把当年的题翻出来看Java部分的考点基本就集中在集合类、并发机制和JVM三大块。集合类里HashMap是永远的C位。题目通常会问默认初始容量是多少负载因子是多少什么时候扩容扩容后是怎么重新分布的线程安全吗为什么线程不安全这几个问题背后有一整套逻辑。默认初始容量16负载因子0.75当元素数量超过容量乘以负载因子时触发扩容扩容后容量翻倍。如果你只记住这些数值那还远远不够因为笔试题经常会换个问法比如“HashMap在并发put时会发生什么”或者“JDK1.7和JDK1.8的插入方式有什么不同”。我在这里多说一句JDK1.7头插法在多线程扩容时可能形成环形链表导致get操作死循环这是当年很经典的一道面试题。JDK1.8改成尾插法并引入了红黑树解决了环的问题但并发下数据丢失仍然可能发生所以正确结论是HashMap本来就不是线程安全的。笔试里如果出现“HashMap是否线程安全”答案很明确不是。需要线程安全就用ConcurrentHashMap。并发这一块volatile和synchronized的对比是出题人最爱。volatile保证可见性不保证原子性synchronized既能保证原子性、可见性还通过管程Monitor机制保证代码块互斥执行。常见的迷惑选项是“volatile能保证原子性”这句话一看就是错的但如果你没理解Java内存模型很容易被选项里“轻量级同步机制”之类的描述带偏。public class VolatileDemo { private volatile boolean flag false; public void stop() { flag true; // 写操作其他线程能立刻看到 } public void run() { while (!flag) { // 循环等待 } } }这段代码是volatile最典型的使用场景——状态标志位。它解决的是可见性问题解决不了“i”这种复合操作的原子性问题。笔试里经常给一段类似的代码问“换成int会不会有问题”“加上volatile后是否线程安全”答案都是否定的。因为volatile只能保证读和写本身是原子的而i涉及读、加一、写回三步volatile管不了这三步之间的并发竞争。JVM部分运行时数据区是必考。程序计数器、虚拟机栈、本地方法栈、堆、方法区每个区域存什么、谁线程私有、谁线程共享这些都得背熟。GC算法里标记-清除、标记-复制、标记-整理各自的优缺点也要能说清楚。选择题里常出现“下列哪些区域会发生OOM”这种组合判断方法区在JDK1.7和1.8之间也有变化1.7叫永久代1.8改成元空间考察的是你是否了解版本的演进。字符串相关的题也经常出。String是不可变的每次拼接都会产生新对象StringBuilder是可变对象单线程下拼接效率高StringBuffer在方法上加了synchronized线程安全但性能略低。笔试问“循环里做一万次字符串拼接应该用哪个”答案不是String而是StringBuilder。这个考点看似简单但代码里到处都是工作里写得好不好笔试就能看个大概。2.2 C/C方向指针、内存与多态阿里研发岗里也有很多C/C岗位所以这套笔试题里C的分量同样不轻。C考题最喜欢在指针、内存和对象生命周期上做文章因为这些点最能暴露一个程序员对底层机制的理解程度。指针和数组的辨析是入门级考点。数组名在大多数表达式中退化为指向首元素的指针但在sizeof和取地址运算符下不会退化这两句结论经常出现在选择题里。举个例子#include cstdio int main() { int arr[5] {1, 2, 3, 4, 5}; printf(%zu\n, sizeof(arr)); // 20 printf(%zu\n, sizeof(arr)); // 8指针大小 return 0; }sizeof(arr)在64位平台上是205个intsizeof(arr)是8指针大小如果把arr传给函数参数函数内sizeof(arr)就是8了因为数组作为参数会退化为指针。这类题考察得很细但确实是日常C程序里绕不开的坑。我当年就在这类题上栽过后来写代码时格外注意数组长度到底应该传到哪里。指针和引用的区别也常考。引用必须在定义时初始化不能重新绑定到其他对象指针可以在任何时候指向其他对象也可以为空。所以函数参数用引用能避免拷贝同时不需要判空用指针则必须检查是否为空。笔试题里问“下面哪个说法正确”选项里经常会藏一个“引用本身不占内存空间”的说法从标准角度严格说引用通常在实现上占一个指针大小的空间但标准没有强制规定不同编译器表现还不一样这种题要特别小心。内存管理是C的重头戏。new和delete要配对使用new[]和delete[]要配对使用malloc和free配合使用这些规则写在任何一本教材里但笔试会用更隐晦的方式考比如问你“下面的代码有没有内存泄漏”或者“析构函数为什么没有被调用”。这里有一个关键点当基类析构函数不是虚函数时通过基类指针delete派生类对象只会调用基类析构函数派生类资源就无法释放。#include iostream class Base { public: ~Base() { std::cout Base destructor std::endl; } }; class Derived : public Base { public: ~Derived() { std::cout Derived destructor std::endl; } }; int main() { Base* p new Derived(); delete p; // 只输出 Base destructor return 0; }这段代码就是多态析构的经典陷阱。正确的写法是把基类析构函数声明为virtual这样通过基类指针delete时才会动态绑定到派生类析构函数先执行派生类析构再执行基类析构。这个知识点几乎每年必考我觉得原因很简单内存泄漏问题在C生产环境里特别难排查笔试直接考察这个点能有效筛掉对对象生命周期没有概念的候选人。C里还有一个高频考点是构造函数和析构函数的执行顺序。构造时先基类后派生类析构时先派生类后基类如果有多个基类按继承声明顺序构造。笔试题经常给出一段多继承代码让你判断输出顺序。这类题没有太多技巧把原则记住再动笔画一遍调用链基本上就不会错。3. 算法与数据结构笔试里的硬通货3.1 排序与查找的复杂度考点算法与数据结构在阿里笔试题里的占比相当可观尤其是排序。你会发现选择题里会反复问到各种排序算法的时间复杂度和稳定性编程题里则会要求你手写其中的某个算法或者用它解决实际问题。先明确一个容易被记混的事稳定性指的是相等元素的相对顺序在排序后是否保持不变。稳定排序有插入排序、冒泡排序、归并排序、基数排序不稳定排序有选择排序、堆排序、快速排序、希尔排序。为什么快速排序不稳定因为partition过程中会交换不相邻的元素相等的元素可能被换到后面去。我记得有一年题目直接给一个数组问“用快排第一轮划分后可能的结果是什么”这种题就得真正理解快排的指针移动过程光背结论是算不出来的。排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n^2)O(n^2)O(1)稳定插入排序O(n^2)O(n^2)O(1)稳定选择排序O(n^2)O(n^2)O(1)不稳定快速排序O(n log n)O(n^2)O(log n)不稳定归并排序O(n log n)O(n log n)O(n)稳定堆排序O(n log n)O(n log n)O(1)不稳定快速排序平均时间复杂度是O(n log n)最坏是O(n^2)最坏情况出现在每次选到的基准都是最大或最小元素时空间复杂度主要来自递归栈平均O(log n)最坏O(n)。堆排序时间复杂度稳定在O(n log n)空间复杂度O(1)但实际运行因为缓存局部性差通常比快排慢。归并排序时间复杂度也是O(n log n)但需要O(n)的额外空间。笔试里经常把这些复杂度混在一起出组合题我建议把每种排序都整理成一张表自己默写一遍比只看书管用得多。手写快排是编程题里的常客代码不长但边界条件特别容易出错。下面是常见写法public void quickSort(int[] nums, int left, int right) { if (left right) return; int i left, j right; int pivot nums[left]; while (i j) { while (i j nums[j] pivot) j--; nums[i] nums[j]; while (i j nums[i] pivot) i; nums[j] nums[i]; } nums[i] pivot; quickSort(nums, left, i - 1); quickSort(nums, i 1, right); }这里有两个细节很容易写错一是内层while一定要先移动右指针再移动左指针二是每个内层循环都要加上i j的边界判断否则会出现数组越界或者死循环。我当年第一次手写快排时就因为在第二个内层循环里漏了i j导致左右指针交错排序结果完全不对。这种错误在笔试里很致命因为阅卷人一眼就能看出你逻辑里有没有洞。查找算法里二分查找也是必考题但往往不是直接考二分本身而是考“在旋转数组中找最小值”或“找第一个大于等于目标值的位置”。这类题目考察的是边界条件的处理能力。我个人建议把二分查找的模板背熟同时搞懂左闭右开和左闭右闭两种写法的区别因为有些算法题换一种边界定义答案就完全不同。3.2 链表、树与栈队列的经典操作链表操作是手写代码题里的常青树。反转链表、判断链表是否有环、找链表中间节点、合并两个有序链表这些都是阿里笔试题里的熟面孔。倒不是因为这些题有多难而是它们能非常直接地考察指针操作的熟练度。链表的题只要把图画出来指针关系理清楚代码就成功了一半。先说反转链表迭代法是最稳妥的public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; // 保存下一个节点 curr.next prev; // 反转指针 prev curr; // 移动prev curr next; // 移动curr } return prev; }写这段代码时最常见的错误是忘记用next变量保存当前节点的下一个节点导致指针移动时丢链。笔试阅卷时看到这种错误基本就知道你对链表还是不够熟。判断链表有环用快慢指针慢指针每次走一步快指针每次走两步如果有环两个指针一定会在环内相遇。这里我建议在草稿纸上手动模拟一次你会发现快慢指针的“追击”关系只和速度差有关只要存在环步长差为1就一定能追上。树的考察通常围绕二叉树展开。前序、中序、后序、层序遍历是基础笔试里更常见的是“根据前序和中序重建二叉树”和“找二叉树中两个节点的最近公共祖先”。这类题在2016年那会儿非常流行因为它融合了递归、分治和边界条件。我见过很多人在重建二叉树时对中序序列里左右子树的区间划分搞不清楚其实核心就一句话前序序列的第一个元素是根在中序序列里找到这个根它左边是左子树右边是右子树然后再递归处理。栈和队列的互相实现也是一类高频题。用两个栈实现队列核心思想是入队时压入stack1出队时如果stack2为空就把stack1里的所有元素倒入stack2再弹出stack2的栈顶。这个方案的均摊复杂度是O(1)因为每个元素最多被移动两次。笔试里如果问“为什么入队操作不需要搬移元素”其实就是想考你对“摊还分析”的理解——单个出队操作可能很慢但长期来看总代价是线性的。这段内容我建议你复习时动手把每一种操作都写在纸上不要用IDE自动补全因为笔试环境通常就是一个简单的在线编辑器没有智能提示平时太依赖快捷键的人手写代码速度会明显下降。语言基础、算法、数据结构这三块笔试题再怎么翻新核心永远跑不出这些。4. 操作系统与网络选择题里的大头4.1 进程线程、死锁与内存管理操作系统在阿里笔试题里占据的篇幅不低尤其爱考进程和线程的区别、死锁的产生条件、虚拟内存与分页这些基础概念。进程和线程这块最经典的问题是“进程和线程的区别是什么”标准答案是进程是资源分配的基本单位线程是CPU调度的基本单位同一进程内的线程共享地址空间而不同进程的地址空间相互隔离。选择题的干扰项通常会把“线程拥有独立的地址空间”写成正确答案或者把“创建线程的开销大于创建进程”作为正确说法这俩都是错的。创建线程比创建进程开销小得多因为线程不需要复制独立的地址空间和资源表。死锁是操作系统选择题里的重头戏。死锁发生的四个必要条件是互斥、持有并等待、不可剥夺、循环等待只要破坏其中一个死锁就能避免。笔试里常考的是“下面哪种策略可以避免死锁”答案往往是银行家算法这个算法通过预判分配后的系统状态是否安全来拒绝不安全的请求属于“避免”策略和“预防”“检测与恢复”要区分开。很多人把“预防”和“避免”这两个概念混在一起但其实预防是静态地破坏必要条件避免是动态地分配前做安全性检查。内存管理部分页式存储、页表、逻辑地址到物理地址的转换是我当年复习的重点。选择题常给一个例子“页面大小为4KB逻辑地址0x1234页表给出页号到物理页框的映射请问物理地址是多少”。解题思路很简单先算出页号和页内偏移再用页表查到物理页框最后物理地址 物理页框号 × 页面大小 页内偏移。地址转换这题看起来唬人其实就是算术题掌握了套路基本就是送分题。还有一个考点是LRU缓存淘汰算法。它不仅在操作系统题里出现在系统设计题里也经常用到。LRU的思路是当缓存满时淘汰最久没被访问的数据。笔试有时候会给你一个访问序列让你写出缓存淘汰的过程或者给一段数组让你判断哪些数据被淘汰。想高效实现LRU需要哈希表加双向链表哈希表负责O(1)查找双向链表负责O(1)插入和删除。这个组合在阿里笔试和面试里都出现过建议直接动手写一遍实现。4.2 TCP/IP与HTTP的必背细节计算机网络部分的题目主要集中在TCP/IP协议栈和HTTP协议。这些概念和生产环境的问题定位关系很密切所以大厂笔试基本都会考。TCP三次握手和四次挥手是必考中的必考。三次握手客户端发送SYN服务器回复SYNACK客户端再发ACK目的是同步序列号并建立连接。四次挥手主动关闭方发送FIN另一方回复ACK然后等自己数据发完再发FIN主动关闭方最后回复ACK。笔试选择题经常问“为什么建立连接需要三次而不是两次”答案是为了防止失效的连接请求突然又到达服务器导致服务器白白建立到不存在的客户端的连接。TIME_WAIT状态是另一个高频考点。主动关闭方在发送最后一个ACK后会进入TIME_WAIT状态持续约2MSL然后才真正关闭连接。原因是一方面要保证最后一个ACK能到达对方如果丢了可以重传另一方面要让本连接中所有旧的报文段在网络中彻底消失防止它们干扰后续使用相同端口的连接。笔试题里常问“TIME_WAIT出现在哪一端”答案是主动关闭连接的一方很多人以为是被动关闭方这个错误很常见。HTTP协议这块状态码的含义是基础。200代表请求成功301是永久重定向302是临时重定向400是客户端请求错误403是禁止访问404是找不到资源500是服务器内部错误502是网关错误503是服务不可用。选择题喜欢把301和302放在一起问区别其实核心就是“永久”和“临时”的差异。HTTP请求方法里GET和POST的对比也比较常考GET把参数放在URL里一般用于查询有长度限制POST把参数放在请求体里更适合提交数据。虽然现在很多人说“POST比GET安全”这种说法不准确因为HTTPS才是真正解决安全问题的方案但写成笔试选择题时按教材里的约定来答基本不会错。DNS解析过程也值得过一遍。整个流程大概是浏览器先查浏览器缓存再查系统hosts文件再查本地DNS服务器本地DNS服务器如果也没有缓存就会从根DNS服务器开始递归或迭代查询逐级找到负责对应域名的权威DNS服务器。笔试题里经常问“下列哪种方式不会发起真正的网络请求”答案是查浏览器缓存和hosts文件这两个环节是本地完成的。这些内容看起来琐碎但恰恰是笔试里抢分的关键因为只要背过就有分几乎不需要计算。5. 现场实战复盘一道编程题从读题到提交5.1 完整解题流程拆解讲完各模块的考点我想用一道典型的笔试题完整演示一下解题流程。就拿“合并两个有序链表”这道题来说它在我印象里是2016年前后非常常见的题型看起来简单但每一步都有考察点。第一步是读题。题目通常这么写输入两个递增排序的链表请合并这两个链表使新链表中的节点仍然递增排序。有些题目描述在示例里给了输入输出有些没有。读题时一定要确认两点链表是否可能为空节点值是否可能重复这两个问题直接影响代码怎么写。如果题干没有明确说我会按“两个链表都可能为空”来处理这样可以避免在边界问题上扣分。第二步是选算法。合并两个有序链表本质上是归并排序中merge操作的链表版本可以用迭代或递归实现。迭代法需要维护一个新链表的当前指针同时用两个指针分别遍历原链表每次取较小值的节点接到新链表尾部最后把剩余部分直接拼接。最终时间复杂度是O(nm)空间复杂度是O(1)。第三步是写代码。我给出一个常见实现public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode dummy new ListNode(0); ListNode tail dummy; while (l1 ! null l2 ! null) { if (l1.val l2.val) { tail.next l1; l1 l1.next; } else { tail.next l2; l2 l2.next; } tail
返回列表