ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

2016美丽联合校招笔试题解析:链表、字符串与动态规划核心考点

2016美丽联合校招笔试题解析:链表、字符串与动态规划核心考点 2016年那会儿移动电商正打得火热美丽联合集团刚把蘑菇街、美丽说这些牌子整合到一起技术团队扩张非常快。那几年的校招笔试题风格比现在要“硬核”不少尤其研发岗上来就是C语言、数据结构、算法一套题做下来基本能筛掉大半人。我印象里这套笔试题在当年校招圈流传很广不是因为难到没边而是它考得非常细很多题目看着眼熟真下手写代码才发现处处是坑。今天把这套题里反复出现的几个核心考点拎出来结合我后来实际工作中的体会做个完整拆解。这套题适合谁看两类人。一类是准备参加校招或者跳槽大厂研发岗的可以通过这套题摸摸底知道基础题到底在考什么另一类是工作了两三年、想回头补补基本功的你会发现当年笔试里那些“变态”细节其实在工作里都遇到过只是当时没意识到。1. 套题印象与考察逻辑1.1 整体风格基础为王代码量为王美丽联合这套2016年的研发工程师笔试题整体风格用一句话概括不玩偏题怪题但把基础题考到极致。整套题大概分三个部分选择题、简答题、编程题。选择题覆盖C语言、操作系统、计算机网络、数据库大概20到30道简答题一般是两三道考概念理解比如进程和线程的区别、TCP三次握手为什么不是两次编程题通常是两到三道考链表、字符串、动态规划这类经典题型。这个命题思路其实很有代表性。电商公司的研发岗尤其是交易、订单、商品这类核心链路对代码稳定性的要求极高。笔试环节不指望你能写出多惊艳的架构而是先确认你有没有扎实的基本功——指针会不会用、边界条件想不想得到、复杂度算不算得清。这些能力短期内突击不出来所以笔试筛人特别准。1.2 隐藏的筛选逻辑不是考你会不会而是考你熟不熟我后来参与过几次校招面试回头看这套题发现它的筛选逻辑在于“熟练度”。同样是反转链表有人五分钟写完有人半小时还在一堆指针里打转差距就在对基础数据结构的熟悉程度上。选择题里那些看似简单的C语言题其实是在考你有没有真正理解内存布局、类型转换、运算符优先级这些底层机制。还有一个容易被忽略的点这套题很注重答题规范。编程题不仅看结果对不对还看代码风格。变量命名、缩进、注释、边界检查这些都是加分项。很多人在笔试时只顾着把核心逻辑写出来忘了处理空指针、数组越界这些情况结果在用例测试环节被扣分。这个习惯如果带到工作上线上出bug的概率会高很多。2. 必考题型深度拆解链表、字符串与动态规划2.1 链表操作反转链表的四种写法与指针陷阱链表题在当年的笔试里几乎是必考的反转链表又是链表题里的“Hello World”。这个题看着简单但能一次性写对的人真不多。先看最基本的迭代写法struct ListNode* reverseList(struct ListNode* head) { struct ListNode *prev NULL; struct ListNode *curr head; while (curr ! NULL) { struct ListNode *next curr-next; curr-next prev; prev curr; curr next; } return prev; }这段代码的核心逻辑就三句话保存下一个节点、反转当前节点的指针、移动prev和curr。难就难在中间那步必须先把next存下来否则一旦执行了curr-next prev原来的下一个节点就找不到了。我当年第一次写的时候顺序搞反了结果链表直接断掉。迭代写法之外递归写法也要会。面试官有时候会追问“能不能用递归实现”这时候你要能立刻写出来struct ListNode* reverseList(struct ListNode* head) { if (head NULL || head-next NULL) { return head; } struct ListNode* newHead reverseList(head-next); head-next-next head; head-next NULL; return newHead; }递归写法的关键在理解“递”和“归”的过程递归到链表的最后一个节点然后逐层反转指针。很多教科书喜欢用递归但实际笔试中迭代写法更稳因为递归在链表很长时有栈溢出的风险。我当时给自己定了个规矩递归和迭代两种写法都要能在五分钟内默写出来。除了反转环形链表的检测也是高频题。用快慢指针快指针每次走两步慢指针每次走一步如果两个指针相遇就说明有环。这个解法时间复杂度O(n)空间复杂度O(1)比用哈希表记录访问过的节点要优雅得多。考题有时候会升级成“找到环的入口”这里需要一点点数学推导其实也不难就是快慢指针相遇后把一个指针挪回头节点两个指针每次各走一步再相遇的位置就是环入口。2.2 字符串处理全排列与最长公共子串字符串类的题目这套笔试题里出现过几道比较有代表性的是字符串全排列和最长公共子串。全排列这个题现在的LeetCode上还在考但2016年那会儿没有这么多刷题平台全靠自己手写递归。核心思路是回溯法逐个固定某个位置的字符递归排列剩下的字符递归回来再换下一个字符试试。def permute(s, start, result): if start len(s) - 1: result.append(.join(s)) return seen set() for i in range(start, len(s)): if s[i] in seen: continue seen.add(s[i]) s[start], s[i] s[i], s[start] permute(s, start 1, result) s[start], s[i] s[i], s[start]注意看这个实现里加了一个seen集合用来去重。如果字符串里有重复字符比如“aab”不去重的话就会生成重复排列。这个细节在笔试里属于拉开差距的点很多人能写出基础版本的递归但忘了去重导致输出结果比预期多。我在实际面试中问过不少候选人能主动处理重复字符的基本代码功底都还不错。最长公共子串LCS是另一道典型题。注意是子串不是子序列子串要求连续子序列不要求。用动态规划做定义一个二维数组dp[i][j]表示以str1[i-1]和str2[j-1]结尾的最长公共子串长度递推公式是如果str1[i-1] str2[j-1]则dp[i][j] dp[i-1][j-1] 1否则dp[i][j] 0这个递推关系很容易理解关键是要想清楚dp数组的维度为什么是(m1)x(n1)而不是m x n——多出来的一行一列是为了处理i0或j0的边界情况让代码逻辑统一。这个技巧叫“哨兵初始化”在很多动态规划题目里都能用。2.3 动态规划入门背包问题与最长递增子序列动态规划是我当年笔试的噩梦也是后来工作中收益最大的一块。美丽联合这套题里动态规划考得不深基本是入门到中等难度但足够筛掉没刷过题的人。背包问题是最经典的DP入门题。我记得题目大致是有一个容量为V的背包n个物品每个有重量w[i]和价值v[i]问能装的最大价值是多少。01背包的代码模板def knapsack(V, weights, values): n len(weights) dp [0] * (V 1) for i in range(n): for j in range(V, weights[i] - 1, -1): dp[j] max(dp[j], dp[j - weights[i]] values[i]) return dp[V]这里有一个必须讲清楚的坑为什么要倒序遍历j因为01背包要求每个物品只能选一次如果正序遍历dp[j - weights[i]]可能在当前物品这一轮已经被更新过就会导致同一个物品被选多次变成完全背包了。这个倒序是面试必问的点一定要能解释明白。顺着这个思路推完全背包的代码就是把内层循环从倒序改成正序代码就两行的差别但含义完全不同。最长递增子序列LIS也是常客O(n^2)的DP解法是基础进阶的O(nlogn)用二分加贪心的思路也要掌握。二分优化的核心思想是维护一个tails数组tails[k]表示长度为k1的递增子序列中末尾元素的最小值遍历原数组时用二分查找找到第一个大于等于当前元素的位置并替换。这个优化的精妙之处在于它用贪心思想保证了tails数组尽量小从而能延伸出更长的递增子序列。3. 实操过程按笔试现场节奏走一遍编程题3.1 拿到题目后前5分钟该干什么很多人在笔试时犯的最大错误是一上来就写代码。拿到题目先花五分钟做三件事第一读清楚题目要求特别注意输入输出的格式、数据范围、有没有特殊说明第二在草稿纸上画一画样例数据手动跑一遍流程验证自己对题意的理解第三确定时间复杂度和空间复杂度的要求判断该用哪种算法。这三步做完再动手写代码你会发现思路清晰很多。我当年参加笔试的时候在草稿纸上写写画画这个习惯救了我好几次。编程题经常会有一句话陷阱比如“输入可能包含多个测试用例”“结果需要对1000000007取模”这些细节一旦漏掉代码写得再对也是白搭。数据范围也很关键如果n的规模是10^5O(n^2)的算法基本必超时必须在设计阶段就排除掉。3.2 写代码时注意的代码规范与命名习惯笔试的编程题很多时候是在纸上或在线编辑器里写代码没有IDE提示也不能编译调试。这种情况下代码规范就显得特别重要。变量命名要见名知义不要起a、b、c这种名字除非是循环变量。函数要短小一个函数只干一件事。重要的逻辑分支要加注释说明自己的思路。这些都是阅卷官能直接看到的东西规范整洁的代码会留下好印象。一个真实的教训我当年笔试时写了一个“求二叉树深度”的递归函数代码缩进乱七八糟变量名用了p、q这种毫无意义的字母。后来面试官反馈说代码逻辑没问题但可读性太差换成实际工作中同事根本没法维护。从那以后我才意识到笔试不光考你会不会做还考你能不能写出“像样”的代码。3.3 用例自测从样例到边界条件的验证过程代码写完以后很多人的习惯是直接提交这是个严重的错误。在提交之前至少要构造几组额外的测试用例来验证代码的正确性。用例要覆盖这几种情况空输入、单元素输入、正常输入、极端输入、包含重复元素的情况。举一个具体例子。如果你写的是求数组最大子数组和经典的Kadane算法测试用例至少要覆盖全负数数组因为这时候最大子数组是单个元素算法要能返回最大的那个负数全正数数组因为这时候整个数组就是最大子数组数组里有正有负这是最常规的情况以及空数组或指针为NULL的情况——注意这里有没有明确说明输入不为空没有的话就要在代码开头做防御性判断。我当时给自己定的标准是常用题目类型的边界条件必须背下来。链表类的题永远先问head为NULL怎么办数组类的题永远先问数组长度为0怎么办涉及索引的题永远检查是否会出现越界访问。这些检查写多了以后就会变成肌肉记忆写任何代码都会顺手加上。4. 高频失分点与排查技巧4.1 数组越界和空指针是最常见的两个坑统计了一下过去几年各种笔试的失分点数组越界和空指针处理排在所有错误里的前两位。这两个问题在笔试的机器判题环节尤其致命——因为在线OJ系统往往会用大量非常规的测试用例来刁难你比如长度为1的数组、只有一个节点的链表、最大值的整数输入。你的代码只要有任何一个用例过不了这道题就不可能拿到满分。数组越界的典型场景二分查找里left和right的更新条件写错导致循环永远退不出去或访问了mid-1的负索引动态规划的DP数组初始化维度错了导致访问dp[i-1]时i为0就越界。空指针的典型场景链表题里访问了curr-next但没检查curr是否为NULL二叉树题里递归时没判断node是否为NULL就直接访问node-val。规避方法就一句话访问任何指针或数组下标之前先确认它合法。虽然这句话说起来简单但做起来真的需要刻意练习。我在LeetCode上刷了大概两百道题之后才真正把这个习惯内化之后写代码出现越界和空指针的概率极低。4.2 时间复杂度不达标从TLE中学到的优化思路很多候选人在笔试时能写出正确但超时的代码这在“时间限制1秒”的机器测试下等同于错误。为什么超时因为数据规模比你想象的大。2016年那会儿很多笔试系统用的是限时判题比如n的规模是10^5你写了个O(n^2)的算法算一下操作数大概是10^10次现代CPU每秒能执行的操作数大约是10^8到10^9量级所以你的代码至少要跑10秒以上必然TLE。如何在笔试时快速判断算法能否通过一个粗略的估算方法if数据规模是 10^5 左右O(n·logn) 或 O(n) 的算法是可以接受的O(n²) 大概率会超时。如果是10^3O(n²) 依然安全甚至可以尝试 O(n³) 的暴力解法。这个“复杂度预算”的概念我每次都讲给团队里的新人比盲目追求高级算法实用得多。4.3 排查问题的思路从现象倒推代码位置笔试时没有IDE的调试工具排查问题只能靠读代码和打印中间状态。我在现场考试时总结了一套排查逻辑先看输入处理是否正确再看循环的边界条件最后查递归的退出条件。绝大概率问题就出在这三处。还有一个技巧在关键循环或递归入口加注释写明当前的逻辑不变式也就是这个位置上的数据应该满足什么性质。如果发现运行结果不符合这个性质说明前面的逻辑有问题。举个例子写归并排序的时候合并两个有序数组的循环里正确的逻辑是“把两个数组中较小的元素依次放入结果数组”。如果循环条件写成了while(left len1 right len2)但没有处理某一个数组提前遍历完的情况运行结果要么漏掉一堆元素要么在越界访问后崩溃。遇到这种情况把print语句放在循环前后分别打印两个子数组的内容一眼就能发现问题。5. 针对这类笔试的复盘方法与长期准备路线5.1 考后复盘比考试本身更重要笔试交卷只是第一步真正的成长在复盘。我建议每做完一套笔试题都要把做错的、不会做的、做对了但思路混乱的题目整理到一个文档里。整理的时候重点记录三样东西为什么错——是概念不清、思路方向错了还是写代码时手滑正确答案的完整推导过程以及同类题的解题模板。这套方法是考研时学来的放在刷题上同样适用。2016年那场笔试过后我把所有错题按“链表类”“字符串类”“DP类”“网络概念类”分好类之后每次刷题都往这个知识体系里填。半年后回头看当初觉得难到不行的题目其实都是同一类套路。用现在的话说就是建立了一个属于自己的算法题“题感库”。5.2 系统准备路线数据结构是骨架算法是灵魂针对大厂研发岗的笔试准备路线上我建议按这个优先级来第一优先是数据结构数组、链表、栈、队列、哈希表、树、堆、图每一种都要能做到十分钟内手写核心操作第二优先是基础算法排序、二分、双指针、滑动窗口、递归、回溯、动态规划、BFS/DFS第三优先级才是计算机网络、操作系统这类理论知识。数据结构的代码要熟练到什么程度我的标准是给你一个空白的代码编辑器你能够在五分钟内写出无bug的链表反转、二叉树前中后序遍历、用两个栈实现队列、手写堆的push和pop。这些是最基本的手艺就像厨师的基本刀工没有人会因为你刀工好给你加分但刀工不好一定减分。5.3 从笔试到工作这些基础能力如何迁移到实际业务最后聊聊一个很多人都没意识到的点笔试里那些看起来“脱离业务”的算法题其实在真实开发中都有对应场景。链表操作对应的是内存管理、缓存淘汰算法LRU就是哈希表双向链表字符串匹配对应的是文本搜索、敏感词过滤动态规划对应的是电商里常见的价格计算、优惠券叠加、库存分配DFS/BFS对应的是商品分类树的遍历、推荐系统的图遍历。拿我自己当年的经历说笔试里考过的一道“最小栈”设计题——支持push、pop、top和getMin要求时间复杂度O(1)——后来在工作中做交易系统的订单状态流转需要随时知道当前订单的最小超时时长用的就是一模一样的思路用辅助栈保存当前最小值空间换时间。当时理解了这个原理工作中做得又快又稳。这也验证了那套笔试题的设计理念基础扎实的人大概率在业务中也靠得住。回头想想2016年那套笔试题考点其实不算偏甚至可以说非常经典。真正让它在校招圈流传的原因是它把每个基础考点都挖得很深——表面考的是链表反转实际上考的是指针操作的熟练度表面考的是动态规划实际上考的是能不能理解状态转移的每一步逻辑。把这些基础点吃透不但能应付笔试对后续的职业发展也是一笔长期投资的财富。
返回列表