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

资讯详情

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

Shopee 2019校招算法真题复盘:五大高频套路与解题思路

Shopee 2019校招算法真题复盘:五大高频套路与解题思路 Shopee 2019校招的编程题放到今天看依旧是一套非常标准的“算法能力体检表”。当年这批题传出来之后很多刷题群都在分析结论出奇一致没有偏题怪题全是 LeetCode 上被人反复做烂了的经典题型但加了一些电商业务里常见的“小变形”。这也恰恰是校招笔试最真实的模样——不是考你会不会做难题而是考你在有限时间内能不能把基础算法用熟、用准、讲清楚。这篇文章我把真题里最高频的几个套路挑出来逐题拆解法思路、代码细节和面试官追问的角度。正在准备秋招的人、刷题遇到瓶颈的人以及想知道电商公司笔试到底在考什么的人都可以拿这份复盘当作参考坐标。1. 题目整体观感这批真题在筛选什么能力1.1 题型分布高频考点集中在四个方向2019年前后电商公司的校招笔试题型基本稳定在四类字符串处理、线性数据结构栈、队列、链表、动态规划、二叉树遍历。这套题目里的栈模拟、滑动窗口、路径DP、层序遍历变体几乎就是这四个方向的代表。考察方向代表题型核心算法模型字符串处理最长无重复子串滑动窗口、哈希集合线性数据结构简化路径栈模拟动态规划最小路径和状态转移、滚动数组二叉树之字形打印BFS、层序遍历为什么是这四个方向而不是更多冷门算法我个人的理解是电商业务里最常打交道的无非是订单状态流转状态机用栈或队列模拟状态变化、用户行为序列字符串处理、滑窗统计、路径和资源规划DP、贪心。算法题出得偏基础恰恰是面试官在考察候选人的基本功下限——一个连这些高频模型都不能熟练驾驭的人放到真实业务里大概率会在抽象建模环节卡住。从难度曲线上看这批题大概有六成是中等难度、三成简单题、剩下可能有一道偏难的压轴但再难也基本不会超出“经典题加一个变换”的范围。这和后来很多互联网公司动辄上困难题的做法形成了鲜明对比。不过不要因此就低估它简单题不等于容易全对笔试判分往往是看测试用例通过率边界条件写漏一个空指针该扣的分一分不少。1.2 从电商业务反推考法这些题不是随便选的很多刷题的人只关注“怎么解”很少有人去想“为什么出这道题”。放在Shopee这类电商公司的场景里题目和业务其实是能对上的。举个例子简化路径那道题本质是在做系统里最常遇到的路径规范化用户在网页端上传文件、后端拼接存储路径到处是脏路径处理合并区间对应的是把多个数据源返回的时间段、库存区间、价格区间合并成一个干净的集合最长无重复子串这种滑窗题往业务上靠就是针对用户点击序列、搜索词序列做无重复行为窗口分析。面试官出题时不见得真想让候选人联系业务但这些题确实是电商后端开发中最常见的算法原型。所以复盘真题最有效的方式不是背答案而是看自己能不能在解题时自然联想到业务里的同类场景。能联想到的说明抽象能力在起作用联想不到的也别着急先把套路练熟抽象能力会在大量做题后慢慢长出来。2. 经典题拆解五道题吃透五种必考套路为什么挑这五道题因为我发现它们恰好覆盖了五个最高频的算法套路滑动窗口、单序列动态规划、栈模拟、二叉树层序遍历、排序贪心。下面逐一拆解代码部分是我在本地跑过验证的版本重点讲“为什么这么写”而不是“怎么写”。2.1 最长无重复子串滑动窗口的“右扩左缩”心法题目描述很简单给定一个字符串找出其中不含有重复字符的最长子串的长度。比如输入abcabcbb答案是 3因为abc是最长的无重复子串。最直觉的做法是枚举所有起点和终点逐字符检查有没有重复复杂度 O(n^2)数据量大一点就超时。优化的关键点在于想明白一件事当右指针往右走遇到重复字符时左指针不需要一步一步试探而是可以直接跳到重复字符第一次出现位置的下一位。这个“右指针负责扩展、左指针负责收缩”的思路就是滑动窗口。代码实现可以这么写def length_of_longest_substring(s: str) - int: left 0 max_len 0 seen set() for right in range(len(s)): while s[right] in seen: seen.remove(s[left]) left 1 seen.add(s[right]) max_len max(max_len, right - left 1) return max_len这里有几个细节值得展开讲。第一set 只负责判断“窗口里有没有这个字符”因为题目只需要长度不需要知道位置所以 set 是合适的选择但如果面试官追问“如果需要输出子串本身怎么办”就要换成字典记录每个字符最近一次出现的位置左指针直接跳到max(left, 该位置 1)。第二while 里的 remove 操作是 O(1) 的每个字符最多进窗口一次、出窗口一次整体复杂度仍是 O(n)。现场讲题的时候我会先说出这三句话“右指针每走一步把新字符加进窗口如果发现重复左指针不断右移直到窗口恢复合法每一步都更新答案。”三句话说完面试官就能确定你是真懂而不是背代码。这道题还有一个常见的变体是“最长无重复子序列”允许删字符那就完全不是滑窗的思路了而要用动态规划审题的时候千万别混淆。2.2 最小路径和动态规划的“原地滚动”技巧题目描述给定一个 m 行 n 列的网格每个格子有一个非负整数找出一条从左上角到右下角的路径要求只能向右或向下移动使得路径上数字的总和最小。这道题是动态规划里最典型的“最短路径类”问题。定义一个状态dp[i][j]表示从起点走到(i, j)的最小代价因为只能从上方或左方过来所以递推关系是dp[i][j] grid[i][j] min(dp[i-1][j], dp[i][j-1])。边界上第一行只能从左往右累计第一列只能从上往下累计。如果允许修改原数组最省空间的写法是原地更新def min_path_sum(grid): if not grid or not grid[0]: return 0 m, n len(grid), len(grid[0]) for i in range(1, m): grid[i][0] grid[i - 1][0] for j in range(1, n): grid[0][j] grid[0][j - 1] for i in range(1, m): for j in range(1, n): grid[i][j] min(grid[i - 1][j], grid[i][j - 1]) return grid[m - 1][n - 1]很多同学第一次看到这种写法会问改了原数组会不会影响后续计算不会因为每一格计算完之后它的值就变成了“到达该格子的最小代价”后续格子只需要读取这两个值即可不需要还原。如果面试官不允许修改原数组就用一维滚动数组来优化空间。核心想法是计算当前行时只需要上一行的完整结果更早的行都不需要保留。所以用一个长度为 n 的 dp 数组每次更新时dp[j]代表“上一行到达第 j 列的最小代价”dp[j-1]更新后代表“当前行到达第 j-1 列的最小代价”两者取 min 再加上当前格子的值即可。从 O(mn) 空间降到 O(n)是这类题在面试中常见的加分点。这道题也可以反过来想从右下角往左上角倒推状态转移方程镜像对称答案是一样的。这种“正向递推还是逆向递推”的选择在很多 DP 题里都存在考的就是你对状态定义的理解深不深。我在复盘时发现很多人刷了十几道 DP 还在套模板就是因为在“逆向”这件事上没想透——其实只要状态定义清晰正推和倒推都只是同一枚硬币的两面。2.3 简化路径栈模拟的“拆分-入栈-出栈”三步法题目描述给定一个 Unix 风格的绝对路径例如/a/./b/../../c/要求返回规范路径。规范路径要求以/开头两个目录名之间只有一个/.表示当前目录需要忽略..表示返回上一级目录。比如输入/a/./b/../../c/输出/c。这道题最漂亮的地方在于它把字符串处理和栈结合得天衣无缝。看到..这种“撤销上一步”的语义应该立刻联想到栈——因为栈天然支持回退操作。第一反应是用双端队列然后模拟路径层级但实际上一个普通栈就够了。思路是先把路径按/拆成若干部分遍历每个部分遇到空字符串或.就跳过遇到..就让栈顶出栈其余字符串入栈。最后把栈里的目录名用/拼接前面再加上根目录的/。def simplify_path(path: str) - str: stack [] for part in path.split(/): if part or part .: continue if part ..: if stack: stack.pop() else: stack.append(part) return / /.join(stack)这里有一个很容易被忽略的细节split(/)之后连续斜杠会产生空字符串比如/a//b会拆出[, a, , b]所以代码里第一步就是跳过空字符串。很多人在现场调半天问题就出在没处理空字符串上。边界情况也要格外注意输入是/时栈为空拼出来是/正确输入包含路径到达根目录之后的..比如/../../abc栈空时不弹出输出/abc也正确。这类题满分很容易写出来但能不能把所有边界条件都覆盖到才是拉开差距的地方。面试官如果追问变体很可能会问“如果路径不是绝对路径而是相对路径”或者“如果不允许输出..这种回退操作怎么办”本质上就是在考察你有没有真正理解栈在这里扮演的角色而不是死记答案。2.4 之字形打印二叉树层序遍历的标志位翻转题目描述请实现一个函数按层打印二叉树第一层从左到右第二层从右到左第三层再从左到右交替进行。这道题的题眼在于“层序遍历”只要你熟悉 BFS剩下的就是每层方向的控制问题。层序遍历的标准做法是用队列把根节点入队每次取出当前层的所有节点把它们的子节点放入下一层。之字形只需要加一个方向标志位偶数层从0开始的奇数索引把结果反转一下即可。from collections import deque def zigzag_level_order(root): if not root: return [] res [] queue deque([root]) left_to_right True while queue: level_vals [] for _ in range(len(queue)): node queue.popleft() level_vals.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) if not left_to_right: level_vals.reverse() res.append(level_vals) left_to_right not left_to_right return res很多人在这一步会卡住不是不会 BFS而是用list.pop(0)去模拟队列导致复杂度变成 O(n^2)。现场要注意用collections.deque的popleft()这是 O(1) 的操作。这个细节本身不难但能在高压环境下条件反射地用对的人并不多。关于翻转时机我建议采用“先攒完整层再统一反转”的方式逻辑清晰代码不容易出错。还有一种常见写法是维护一个双端队列奇数层从尾部塞节点、偶数层从头部塞节点不用最后 reverse但理解成本更高现场容易把自己绕晕。我个人的习惯是优先保证正确性和可讲性再考虑代码精致度。二叉树这类题还有一个高频变体是“从底部向上层序遍历”做法是把最终结果res整个反转或者用collections.deque的appendleft逐层插入头部面试时可以主动提一下能体现你对 BFS 的掌握程度。2.5 合并区间排序贪心的“重叠判定”边界题目描述给出若干区间[start, end]请合并所有有重叠的区间。比如[[1,3], [2,6], [8,10], [15,18]]合并后是[[1,6], [8,10], [15,18]]。合并区间是排序加贪心的经典代表也是电商场景里很常用的抽象模型比如把多个时间窗口合并、把多段价格区间合并成最终价格。解法非常固定先按每个区间的 start 升序排序然后遍历区间如果当前区间的 start 小于等于结果中最后一个区间的 end说明有重叠合并时取两者 end 的较大值否则直接加入结果。def merge(intervals): if not intervals: return [] intervals.sort(keylambda x: x[0]) merged [intervals[0]] for cur in intervals[1:]: last merged[-1] if cur[0] last[1]: last[1] max(last[1], cur[1]) else: merged.append(cur) return merged这里最容易踩的坑有两个。第一排序时如果用 lambda 只按 start 排序end 顺序无所谓因为合并逻辑里会重新计算 end第二判断重叠时边界条件取不取等于号很关键题目如果说“区间相切也算重叠”那cur[0] last[1]就是对的如果说“相切不合并”就要把改成。很多笔试用例就是在这种边界上卡人建议把“重叠判定条件”这句单独讲给面试官听。另外一个隐蔽的坑是如果 intervals 的元素是不可变的 tuple直接写last[1] max(...)会报错。笔试环境里通常给的是 list但保险起见可以先转成可修改的结构再做合并。这个细节在 LeetCode 上没什么问题但在某些自定义测试容器里可能让你浪费五分钟。做完这道题我一般会主动说一句“排序复杂度是 O(n log n)遍历是 O(n)总复杂度 O(n log n)空间是 O(n)”。说出来不是为了炫技而是让面试官知道你对自己的代码有全局认知这是所有刷题人应该在现场养成的习惯。3. 笔试和面试之间为什么能把思路讲清楚比做对更值钱3.1 面试官围绕真题的四种追问方式2019年这批校招题在笔试阶段只需要交代码但等到现场面试很多题会被翻出来二次追问。面试官手里拿着一道你已经做过的题通常有四条追问路径。第一种是让你证明复杂度问“这个解法为什么是 O(n)有没有可能退化”比如滑窗口法的 set 操作明明是常数级但如果字符集特别大哈希碰撞会不会变慢。第二种是问边界情况空输入、单元素、重复值、超大数组每道题都要自己主动讲一遍。第三种是问变体解法把题目条件改一个词比如“最长无重复子串”改成“至少包含两个重复字符的最长子串”看你能不能快速迁移思路。第四种最狠直接拷问业务映射你说这个算法在系统里有什么用这四种追问没有一种是靠背题能应付的。所以我在复习阶段给自己定了一个规矩每做完一道题不看题解先自言自语把思路讲一遍再尝试回答至少两个“如果……会怎样”的追问。练得多了现场被追问时反而会觉得是在聊题而不是在被审问。3.2 现场讲题的“三段式表达法”刷题群里有句话叫“刷题五分钟讲题两小时”虽然夸张但道理是真的。面试时的算法题代码只占一部分评分表达占另一半。我推荐的表达框架是三段式第一段先说思路不要急着写代码“这是一道经典的滑动窗口题我用左右两个指针维护一个无重复窗口右指针扩展遇到重复就收缩左指针整个过程每个字符最多进出一次所以时间复杂度是 O(n)。”第二段说复杂度“空间上用一个 set 存窗口内字符最坏情况是整个字符串无重复所以是 O(n)。”第三段才开始写代码写的时候把关键判断用简短注释标出来重点解释那一行的作用。看起来很简单但大多数候选人实际操作时都是先动手写写一半发现和思路对不上又开始打补丁。正确的顺序应该是先想清楚再动笔代码一蹴而就这种稳定感本身就会给面试官留下好印象。3.3 笔试环境的时间分配与取舍策略笔试和现场面试还有一点不同笔试没有人和你互动时间一到系统自动交卷所以策略更重要。我的经验是拿到题先花两分钟把全部题浏览一遍按难度和熟练度排个序。会做的、有思路的先写卡住超过15到20分钟就果断跳过回头再补。笔试判分通常按测试用例通过率计算所以“部分正确”也值得拿分。哪怕只能写出暴力解法也要先交上去把 60% 的测试用例保住再去优化。很多同学有一个错误习惯一道题非要一次写对才继续下一道结果前面的题全空最后分数难看。事实上校招笔试的时间设计本来就预留了“先用暴力拿基础分再优化拿进阶分”的空间。另外要注意的是笔试环境没有智能提示也没有单元测试帮你验证。写完代码后一定要在草稿纸上模拟几个典型用例手算一遍再提交。我吃过一次亏某道栈模拟题在本地 IDE 能跑在线环境因为输入格式多了一个换行符导致所有用例全挂从那以后我再也不敢跳过“手推边界用例”这一步。4. 复盘与实战心得那些复习时容易被忽略的细节4.1 刷题框架化而不是刷题数量化现在
返回列表