十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

小米2019秋招软件开发笔试A卷解析:从考点到编程题全复盘

小米2019秋招软件开发笔试A卷解析:从考点到编程题全复盘 2019年秋招季小米的软件开发笔试题A卷是不少计算机专业应届生投递简历后的第一道坎。当时各大求职讨论区里关于这套题的帖子能翻好几页有人吐槽选择题考得太细有人说编程题看起来不难但一提交就超时还有人在求多选答案。现在回头看这套题其实非常典型它考察的不是某一套花哨的技术栈而是大学四年计算机核心专业课的实际掌握程度以及拿到一个问题之后能不能快速写出正确且高效的代码。这套试卷适合谁来参考如果你是大二大三的学生可以通过它提前感受大厂笔试的难度和范围如果你是正在准备校招的应届生它是一份值得反复刷的基础题集就算你已经做了几年开发回头再做一遍也会发现很多平时凭感觉写出来的代码底层原理早就在这套卷子里出现过。笔试和面试最大的不同在于面试可以聊项目、讲故事笔试只能拼基础和算法的熟练度会就会不会就不会这是大厂在简历筛选之后做的第二道机器化过滤。1. 试卷整体画像三个板块外加一份考点地图1.1 卷面结构选择题为主编程题压轴A卷的整体结构和大厂校招笔试的主流模式一致客观题单选加多选占大头最后安排一到两道编程题。时间一般控制在90分钟左右有些考场会放宽到120分钟但大多数人感受到的依然是题量大、时间紧。当年考完后很多人的第一反应不是题太难而是再给我十分钟我还能多做对两道选择。客观题覆盖C/C或Java语言、数据结构、操作系统、计算机网络、数据库偶尔还会出现一两道Linux命令或设计模式相关的题。编程题则集中在字符串处理、链表操作和动态规划这几类常见的出题范畴。分值上编程题往往占30%到40%是决定能否进入面试环节的关键。为什么笔试要这样设计核心原因是海量简历需要高效筛选。选择题可以自动判分投递系统几秒钟就能给出一份成绩排序编程题需要在线评测系统跑测试用例能有效过滤“简历写得漂亮但代码能力欠佳”的候选人。所以这套卷子筛选的其实是两个维度的能力知识储备的广度以及代码实现的基础功。1.2 考点地图把大学四年专业课浓缩成一张表我根据自己刷过的多套同类笔试题把这类试卷的考点整理成一张表。做套题之前建议先扫一遍这张表心里有个谱知识模块题量参考按40题计常见出题形式核心考点示例C/C/Java语言8-10阅读代码写出输出、概念辨析虚函数机制、引用与指针、集合线程安全数据结构与算法8-10复杂度计算、二叉树遍历、排序原理时间空间复杂度、哈希冲突、链表操作操作系统4-6进程线程、死锁、内存管理虚拟内存、死锁四条件、进程调度计算机网络4-6TCP/UDP、HTTP、网络设备三次握手、状态码、DNS查询流程数据库2-4索引、事务、SQL语句B树、ACID、聚簇索引与非聚簇索引Linux与设计模式2-3命令含义、模式识别grep参数、单例与工厂模式这张表不是猜题而是从历年大厂校招笔试的公开面经里统计出来的高频分布。用一个不严谨但直观的说法如果把这张表的每个知识点吃透一套笔试卷子至少能拿到六成以上的分数。剩下的四成靠的是刷题量和临场状态。为什么这套卷子值得反复做因为它的考点足够“正”。它没有偏题怪题考察的都是工程师日常工作中真正会产生影响的计算机基础。这种命题思路代表了大厂对校招生的期望你可以没有丰富的项目经验但底层知识必须扎实因为后面所有的业务开发、系统设计、线上问题排查都建立在这些基础之上。2. 选择题高频考点每个知识点背后都有真实工作场景2.1 语言基础从语法规则考到内存级机制语言题是选择题里最让人头疼的部分。我印象很深的一类题是给出一段C代码让你判断虚函数调用的输出。这里涉及的不只是知道“虚函数存在”而是要理解它到底怎么实现。编译器会把含有虚函数的类变成一个虚函数表表中存的是该类的虚函数地址每个对象内存布局的最前面会有一个虚指针指向这张表。当通过基类指针调用虚函数时程序会顺着虚指针找到虚表再定位到实际函数地址从而实现运行时多态。这道题的常见坑是构造函数里调用虚函数不会发生动态绑定。原因是对象构造期间虚表指针还处于初始化阶段编译器会把它当作当前类的调用处理。很多人在这里答错说明平时只看语法书、没有动手看汇编或调试过内存布局。同理析构函数为什么建议声明为虚函数因为如果基类析构函数不是虚的通过基类指针delete派生类对象时只会执行基类的析构逻辑派生类里申请的资源就会泄漏。这是一个极其真实的工程问题不是笔试造出来的概念题。Java方向的题也有类似的套路。Integer在-128到127之间会走缓存池所以用比较两个Integer时在这个范围内可能返回true超过范围反而要equals稍微绕一下就容易掉坑。HashMap不是线程安全的多线程写会丢数据甚至造成CPU飙升HashTable虽然安全但是全局锁性能差ConcurrentHashMap在JDK8之后取消了分段锁改用CAS加synchronized锁桶的方式。这些知识点没有一项是“背下来就能加分”的全都对应着真实场景比如缓存服务为什么内存占用异常、高并发下HashMap为什么会把机器拖垮。2.2 数据结构与算法手感和数学敏感度都要有选择题里的数据结构题最常考的是时间复杂度和经典结构特性。比如二分查找为什么是O(log n)因为每一轮都把搜索区间缩小一半归并排序为什么稳定的同时还要O(n)的额外空间因为它合并时需要一个辅助数组。这种题不需要死记结论关键看能不能画出递归树或者写出递推公式。二叉树遍历是另一个高频区。前序、中序、后序的递归写法大多数人都能默写但考场上经常出的是“已知前序和中序求后序”或者“判断某序列是不是合法的二叉搜索树前序遍历”。这类题其实在考察对遍历过程的本质理解前序第一个节点是根中序里根把左右子树切开递归套用就能还原整棵树。如果只是背了“递归三步走”遇到变形题就容易卡壳。哈希冲突解决方式也需要分清。链地址法是每个桶后面挂一个链表同样的散列结果排在同一个桶的链表里开放定址法是在冲突位置往后探测空位。理解这两者的区别在实际中也有用比如Redis的字典就用了链地址法Java的HashMap在链表过长时会转成红黑树来保证查询效率。考到这类题时如果能把原理和工程实现联系起来答案会非常清晰而不是靠猜。2.3 操作系统与网络排查线上问题离不开的语言操作系统题里进程和线程的区分几乎年年出现。进程是资源分配的最小单位每个进程有独立的地址空间线程是CPU调度的最小单位同一进程内的线程共享地址空间和资源。因为线程共享内存所以线程间的通信成本比进程间低很多但也正因为共享才需要加锁才容易死锁。死锁的四个必要条件——互斥、持有并等待、不可剥夺、循环等待——在笔试里很常见。对应的破解方向也固定让资源可共享来破坏互斥、一次性申请所有资源来破坏持有并等待、允许抢占来破坏不可剥夺、按固定顺序申请资源来破坏循环等待。这套理论在分布式系统里已经被扩展成“分布式锁顺序问题”。我印象很深的一次线上故障两个服务互相等待对方的锁日志里全是超时告警当时第一个想到的就是循环等待顺着这个思路去梳理调用链很快就定位到了问题节点。计算机网络题同样是送分题和送命题并存。TCP三次握手的过程要理解到每一个标志位的含义第一次握手客户端发送SYN表明请求建立连接并携带初始序列号第二次握手服务端发送SYNACK表示收到客户端序列号同时确认自己的序列号第三次握手客户端发送ACK告知服务端连接建立。为什么不是两次因为如果只有两次握手服务端无法确认客户端是否收到了自己的SYNACK万一客户端因为网络问题没收到服务端会一直维护一个半连接资源浪费系统资源。这个场景放到现在看恰好对应着SYN Flood攻击的防护逻辑。HTTP状态码也是高频考点。301是永久重定向302是临时重定向304表示资源未修改可继续使用缓存404是请求资源不存在500是服务器内部错误503是服务暂时不可用。这些状态码在实际开发里天天见排查接口报错时先看状态码基本就能判断是客户端问题还是服务端问题。2.4 数据库与Linux后端工程师的日常积累数据库部分的重点在索引和事务。B树索引的结构要理解到位非叶子节点只存放键值和指针叶子节点存放真实数据而且叶子节点之间通过链表相连非常适合范围查询和顺序访问。聚簇索引的叶子节点直接存储整行数据一张表只能有一个聚簇索引非聚簇索引的叶子节点存储的是主键值查询列不在索引里时需要回表。理解了回表就能理解为什么建立联合索引时“最左前缀”原则那么重要。事务ACID四个性质里一致性是最终目标原子性、隔离性、持久性都是为实现一致性服务的。隔离性又引出四种隔离级别读未提交、读已提交、可重复读、串行化。MySQL默认是可重复读并通过MVCC和间隙锁解决幻读问题。笔试里常出“某个隔离级别下会出现什么问题”这类题本质上是在考对隔离级别演变脉络的理解。Linux相关的题占比不高但很实际。比如grep -r做递归搜索ps -ef看进程top看系统负载netstat查端口占用。有人觉得这些是运维的活但作为软件开发排查线上环境时第一件事就是登录服务器看进程和日志不会这些命令会非常被动。3. 编程题实战复盘三道典型题从读题到完整代码编程题是笔试的重头戏。A卷的编程题在题型上偏好基础题但会刻意增加边界条件和数据规模的问题。我按考后圈子里复现讨论的常见版本整理成下面三道经典题型题型、难度和考点基本对齐。3.1 滑动窗口解决最长无重复子串题目描述给定一个字符串找出其中不含有重复字符的最长子串的长度。暴力解法是枚举所有子串再用一个哈希表判断是否有重复字符整体时间复杂度O(n^2)。当字符串长度达到十万级别时这个复杂度直接超时。正确解法是滑动窗口加哈希表。核心思路是维护一个窗口窗口左边界left右边界i遍历字符串时把当前字符作为右边界。用哈希表记录每个字符最近一次出现的位置。当遇到重复字符并且该字符上次出现的位置在left右侧时把left跳到上次出现位置的下一位。每一步都更新窗口长度最大值。#include string #include unordered_map #include algorithm using namespace std; int lengthOfLongestSubstring(string s) { unordered_mapchar, int lastPos; int left 0, ans 0; for (int i 0; i s.size(); i) { char c s[i]; if (lastPos.count(c) lastPos[c] left) { left lastPos[c] 1; } lastPos[c] i; ans max(ans, i - left 1); } return ans; }需要注意两个细节。第一判断条件里必须带lastPos[c] left否则有可能把窗口之外的旧位置也纳入比较导致left错误回退。第二遇到重复字符时left更新为上次出现位置加一不需要做额外再检查因为当前i作为右边界窗口内如果还有重复会在后续遍历中继续更新。这段代码的时间复杂度是O(n)空间复杂度O(min(字符集大小, n))。笔试时这种题一定要当场自测几个边界用例空串返回0单字符返回1全重复字符串比如“aaaa”返回1前面重复后面不重复的字符串比如“abba”返回2。这些用例能帮你发现很多隐性问题比如全重复字符串里left会一路往后跳但ans不会变逻辑依然正确。3.2 反转链表两种写法都要能在五分钟内默写题目描述反转一个单链表。反转链表是链表题里最基础的题但也是现场出错率最高的一道。迭代法的核心是三个指针。pre指向已经反转好的链表的头节点curr指向当前要反转的节点next保存curr的下一个节点防止断链。每次循环把curr的next指向pre然后pre、curr、next都向后移动一位。struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* pre nullptr; ListNode* curr head; while (curr ! nullptr) { ListNode* next curr-next; curr-next pre; pre curr; curr next; } return pre; }递归写法理解起来稍微难一些但代码更短。递归的终止条件是head为空或head-next为空。每层递归先反转head之后的部分拿到新的头节点然后把head自己放到链表尾部。ListNode* reverseListRecursive(ListNode* head) { if (head nullptr || head-next nullptr) { return head; } ListNode* newHead reverseListRecursive(head-next); head-next-next head; head-next nullptr; return newHead; }递归写法的关键在于“相信函数定义”reverseListRecursive(head-next)返回的是以head-next为头节点的子链表反转后的新头部。拿到这个新头后只需要把当前head接在后面。最后返回的是newHead而不是head。很多人递归写错就是把head-next-next head这一步想反了。笔试时如果时间紧张写迭代法更稳妥递归法容易因为对栈理解不够而出现思路混乱。建议两种都练到能默写的程度因为面试时面试官很可能会追一句“用递归再写一版”。3.3 动态规划解决最长递增子序列题目描述给定一个无序的整数数组找到其中最长递增子序列的长度。这是一道非常经典的动态规划题。先讲朴素DP。定义dp[i]表示以nums[i]结尾的最长递增子序列长度初始值为1。对于每个i遍历之前所有j如果nums[j] nums[i]说明可以接在nums[j]后面dp[i]更新为max(dp[i], dp[j] 1)。遍历结束后答案取dp数组的最大值。#include vector using namespace std; int lengthOfLIS(vectorint nums) { int n nums.size(); if (n 0) return 0; vectorint dp(n, 1); int ans 1; for (int i 1; i n; i) { for (int j 0; j i; j) { if (nums[j] nums[i]) { dp[i] max(dp[i], dp[j] 1); } } ans max(ans, dp[i]); } return ans; }朴素DP的时间复杂度是O(n^2)当数组长度到一万以上就会比较吃力。若题目要求更优解可以用贪心加二分维护一个tails数组tails[k]表示长度为k1的递增子序列的末尾元素的最小值。遍历每个数字x在tails里查找第一个大于等于x的位置并替换如果不存在说明x比所有末尾元素都大可以扩展子序列长度。#include vector #include algorithm using namespace std; int lengthOfLIS(vectorint nums) { vectorint tails; for (int x : nums) { auto it lower_bound(tails.begin(), tails.end(), x); if (it tails.end()) { tails.push_back(x); } else { *it x; } } return tails.size(); }这个做法的时间复杂度降到O(n log n)但理解难度也上来了。我建议初学者先把O(n^2)的DP练熟因为动态规划的思路是通用的很多变种题都是在这个基础上加条件。O(n log n)的做法能理解证明过程最好实在有困难也可以先记住模板大多数笔试的数据规模用O(n^2)也能通过。4. 答题节奏与应试策略90分钟到底该怎么分配4.1 选择题30到40分钟必须收住笔试最怕的不是不会而是会做的题没时间做。选择题如果做得太慢后面的编程题就只能草草提交。我给自己定的节奏是单选控制在30秒到1分钟一题多选1到2分钟一题35道左右的选择题加起来不超过40分钟。遇到读两遍还没思路的题立刻跳过先标记一下写完编程题再回来看。这里有个实际经验很多人觉得多选难拿分因为漏选和错选都不得分所以一纠结就耗过去好几分钟。我的建议是对于没有把握的多选选一个最有把握的选项就好。虽然不能保证拿满分但至少能保住一部分分数。从整个卷面的收益来看把犹豫的时间花在编程题上更划算因为编程题一题的分值能顶好几道选择。4.2 编程题先保第一题稳定提交编程题通常有两道难度略有差异。我的策略是先把最有把握的题完整写出来确保通过尽可能多的测试用例再回头攻难题。不要死磕一道题超过20分钟尤其是当第二道题明显需要更复杂的数据结构时。在线评测系统有一个特点编译错误和运行时错误都不给分。写完代码后一定要自己过一遍逻辑确认没有数组越界、没有递归出口写错、没有把变量名搞混。很多时候因为一个分号或者一个大小写问题整道题直接零分这是最亏的。我在笔试时养成一个习惯写完核心逻辑后先在草稿纸上推两个简单用例手工模拟一下过程再点提交能避免大量低级错误。4.3 拿到题目先想清楚再动手编程题常见的低级错误是用错数据类型导致溢出。比如题目给的数据范围是10^9用int就会在中间计算时爆掉结果全错。建议看到题目先看一眼数据范围的提示凡是涉及大数的地方直接用long long。还有一个很常见的问题是读题不仔细题目要求升序代码里写的是降序这种错误没有任何技巧可以弥补只能靠多花30秒把题读清楚。动态规划题如果状态定义对了递推公式就水到渠成如果状态设计错了写出来的代码再长也没有意义。所以在动笔之前先把状态定义、初始化、转移方程、答案位置这四个要素写在草稿纸上。我每次笔试都会提醒自己宁可多花三分钟想清楚也不要边写边改后者才是真正的浪费时间。5. 复盘之后的长期价值从应付笔试到理解工程师的底层能力5.1 当年的高频考点今天依然是面试必问如今再看这套A卷会发现一个很有意思的现象当年选择题里考的那些点在后续的面试中被反复深挖。虚函数机制变成了“讲讲多态在内存里是怎么实现的”TCP三次握手变成了“出现大量TIME_WAIT连接是为什么、该怎么处理”B树索引变成了“联合索引为什么最左匹配使用索引时怎么避免回表”。这说明笔试并不是孤立的关卡它是整个校招流程的知识底座。面试官默认你笔试时已经掌握了这些基础概念所以面试时不再问“是什么”而是直接问“为什么”“怎么办”。如果只是背了答案通过了笔试后面面试一定会露馅。反过来如果认真研究了这些考点背后的原理面试中不管话题怎么延伸都能接得住。5.2 基础知识的掌握程度决定了工作能走多深做过几年开发之后再回头看这些题我对“基础知识”这个词有了完全不同的理解。写业务代码时确实不需要每天手写红黑树但当你需要排查一个线上接口为什么时不时抖动时网络、操作系统、数据库的知识会一起发挥作用先看TCP连接是否异常再看内存使用情况然后查SQL有没有走到索引。任何一个环节的缺失都可能导致问题定位到一半就卡住。这也是为什么我建议还在学校的读者不要只刷题每做完一套卷子把错题对应的教材章节拿出来读一遍。笔试题的价值不在于“考完就忘了”而在于它像一面镜子照出你知识体系里真正薄弱的地方。把每个考点想明白后面遇到的很多真实问题都会变得有迹可循。5.3 给准备校招的朋友几条实在建议第一至少刷200道分类题字符串、链表、二叉树、动态规划、回溯这些高频类型都要覆盖到。第二操作系统、网络、数据库这三门课不要只看资料尽量结合真实场景去理解比如你现在用的这台电脑上打开一个网页到底发生了什么把这条链路里的每一步都用学过的知识解释出来。第三不要只刷题不总结建议做一个错题本把每次笔试面试中暴露的知识盲区记录下来隔一段时间集中回顾。我自己的做法是把每套刷过的卷子按考点拆成一个个小卡片正面写一个问题背面写原理和典型场景。考前翻卡片比重新刷题效率高得多因为那是针对自己薄弱点的定向复习而不是把时间浪费在已经会的内容上。最后分享一个我踩过几次坑之后的小习惯笔试前把常用数据结构和算法的模板代码提前准备好不需要背整段但至少知道到考场上翻哪个模板。往年我总在现场写HashMap遍历时忘了怎么取键值对或者把二分查找的边界写错后来我在本地维护了一个模板文件考试前花十分钟过一遍效果非常明显。这套A卷可能只是你求职路上众多试卷中的一套但如果你能从中真正提取出自己的薄弱点它的价值会远超一场笔试的分数。
返回列表