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

资讯详情

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

Shopee校招编程题核心考点拆解:算法模板与实战解题套路

Shopee校招编程题核心考点拆解:算法模板与实战解题套路 1. 这套编程题到底在考什么每年秋招春招总有同学私信问我同一类问题大厂校招编程题到底怎么准备刷了多少题才够题目风格是不是都差不多。我的回答一直是那句老话别光看题量要看题型结构和出题逻辑。今天拿Shopee 2019校招的这套编程题当样本把题目背后的考点逐个拆开顺便把我自己刷题和带人时总结的解题套路一并放出来。Shopee的校招编程题有一个很明显的特点题目不算偏门算法难度基本集中在LeetCode Medium上下偶尔冒出一道接近Hard的题但绝对不靠“背板子”就能过。它更看重的是你能否把常见算法灵活套用到实际业务场景里。这和Shopee的电商基因一脉相承——电商系统里充满了海量数据处理、价格计算、优惠叠加、订单状态流转这类问题所以编程题也偏向这些方向。整套题大致可以归成几类字符串处理与模拟贪心策略动态规划栈与单调栈应用双指针与滑动窗口基础数据结构哈希表、堆、链表我翻了翻当年的题目汇总从题面描述来看几乎没有一道是纯粹为了难而难的脑筋急转弯绝大多数题都有明确的业务场景外壳。这其实是个信号面试官想看到的不只是你会背算法模板而是你能读懂题目背后的真实诉求把场景翻译成数据结构再把数据结构落成代码。对于准备校招的同学来说这套题的价值在于它代表了一类典型的“中等难度偏业务化”的命题风格。如果你能把这类题刷透再去应付其他互联网公司的校招笔试基本不会慌。2. 典型题目拆解与思路还原2.1 字符串处理括号匹配的变体先说一道经典的字符串题题面大致是给定一个只包含括号字符的字符串判断括号是否合法并要求计算最长合法括号子串的长度。这道题外观是“括号匹配”但它实际考的是栈和动态规划两个方向。如果只是判断整个字符串是否合法那是入门级的栈应用。但一旦要求“最长合法子串”难度立刻上一个台阶因为子串必须是连续的不能简单地靠出栈入栈次数来判断。我当时带人刷这道题时会先让他写一个纯栈的版本#include bits/stdc.h using namespace std; int longestValidParentheses(string s) { stackint st; st.push(-1); // 哨兵作为最后一个无法匹配的位置 int maxLen 0; for (int i 0; i (int)s.size(); i) { if (s[i] () { st.push(i); } else { st.pop(); if (st.empty()) { st.push(i); // 当前右括号无法匹配作为新的基准 } else { maxLen max(maxLen, i - st.top()); } } } return maxLen; }关键在于栈里存的不是括号本身而是下标。哨兵元素-1的作用是处理边界情况当整个字符串从头开始就是合法子串时我们需要一个基准位置来计算长度。每遇到一个右括号先出栈如果栈空了说明这个右括号没有匹配的左括号那就把它自己推入栈中作为后面合法子串的新起点如果栈不空说明存在一个可能的合法子串长度就是当前下标减去栈顶元素。这道题的坑点在于很多人会写成“遇到右括号就检查栈顶是不是左括号”这在只判断合法性的时候没问题但计算最长长度时会导致子串起点计算错误。用下标入栈的方式就能把起点和长度一起维护起来。面试时如果能顺带说出“另一种解法是动态规划dp[i]表示以第i个字符结尾的最长合法子串长度”会显得你思路更开阔int longestValidParenthesesDP(string s) { int n s.size(), ans 0; vectorint dp(n, 0); for (int i 1; i n; i) { if (s[i] )) { if (s[i - 1] () { dp[i] (i 2 ? dp[i - 2] : 0) 2; } else if (i - dp[i - 1] 0 s[i - dp[i - 1] - 1] () { dp[i] dp[i - 1] 2 (i - dp[i - 1] 2 ? dp[i - dp[i - 1] - 2] : 0); } ans max(ans, dp[i]); } } return ans; }动态规划版本虽然写起来绕但它体现的是“状态转移”思维这在后续面试其他DP题时很加分。建议两种解法都熟练掌握。2.2 贪心策略区间调度问题的变种Shopee的题里还有一类很典型的贪心题和“活动安排”高度相似。题面通常会包装成有若干任务每个任务有开始时间和结束时间同一时刻只能做一个任务问最多能完成多少个任务。这类题的标准解法是按结束时间排序然后从左到右贪心地选取。为什么按结束时间排序而不是按开始时间因为结束时间越早的任务留给后续任务的时间窗口就越大。这是贪心算法里最经典的“局部最优推导全局最优”的例证。struct Task { int start, end; }; int maxTasks(vectorTask tasks) { sort(tasks.begin(), tasks.end(), [](const Task a, const Task b) { return a.end b.end; }); int count 0, lastEnd -1; for (auto t : tasks) { if (t.start lastEnd) { count; lastEnd t.end; } } return count; }这里有个细节如果任务的结束时间相同要不要按开始时间再排严格来说不需要因为在结束时间相同的情况下选任何一个都不影响后续的可用时间窗口。但如果开始时间和结束时间都是整数且可能存在“上一个任务结束时间等于下一个任务开始时间”的情况那判断条件要写而不是。这个细节很容易翻车。我见过不少人在笔试时把写成导致边界情况出错丢掉了本该拿到的分数。边界条件恰恰是笔试判分最看重的地方编译器不会替你想这些。2.3 基础DP背包问题的简单包装还有一道题比较典型题面大致是每个商品有一个价值和体积背包有容量上限问能装下的最大总价值是多少。这个就是0/1背包。虽然它不难但Shopee在2019年的题目里把它包装成了“促销凑单”的场景商品品类不同凑单规则略有差异。0/1背包的标准状态转移方程是dp[j] max(dp[j], dp[j - weight[i]] value[i])注意遍历顺序必须是倒序。如果正序遍历就会导致同一个商品被重复选择多次变成完全背包问题。这个“一维数组倒序遍历”的细节几乎是背包类题的第一大坑。int knapsack(int capacity, vectorint weights, vectorint values) { vectorint dp(capacity 1, 0); for (int i 0; i (int)weights.size(); i) { for (int j capacity; j weights[i]; j--) { dp[j] max(dp[j], dp[j - weights[i]] values[i]); } } return dp[capacity]; }如果题目里说“每件商品可以拿任意多件”那就是完全背包遍历顺序改为正序。如果再多一个“商品之间有依赖关系”那就要考虑分组背包或树上DP。校招笔试题一般不会考到树上背包那么深但0/1背包和完全背包的区分是必须烂熟于心的。2.4 单调栈下一个更大元素Shopee这套题里有一道很典型的单调栈题给一个数组对每个元素求出右边第一个比它大的元素不存在则输出-1。为什么这里要用单调栈而不是暴力暴力法的时间复杂度是O(n^2)在数据量达到10^5时基本跑不完。单调栈可以把每个元素至多入栈一次、出栈一次整体复杂度降到O(n)。vectorint nextGreaterElement(vectorint nums) { int n nums.size(); vectorint res(n, -1); stackint st; for (int i n - 1; i 0; i--) { while (!st.empty() st.top() nums[i]) { st.pop(); } res[i] st.empty() ? -1 : st.top(); st.push(nums[i]); } return res; }两种写法都能通过关键是要说清楚单调栈维护的是什么从栈底到栈顶严格递减。当新元素比栈顶大时不断弹出直到栈顶比新元素大此时栈顶就是右边第一个更大的元素。笔试时用从右往左遍历的方式更直观一些因为不需要额外记录元素下标如果题目要求输出“右边第一个更大元素的下标”而不是元素值那就需要改成从左往右遍历、栈里存下标、计算结果时用下标相减。别看只差这一点很多人在考场上就是栽在这种变量上。2.5 双指针有序数组去重与两数之和双指针是面试中性价比最高的技巧之一。Shopee的题里也出现了一道两数之和的变形给定一个有序数组和一个目标值找出数组中两个数使它们的和等于目标值返回它们的下标。如果数组是无序的最直观的做法是用哈希表存“数值—下标”的映射关系遍历一遍搞定vectorint twoSum(vectorint nums, int target) { unordered_mapint, int mp; for (int i 0; i (int)nums.size(); i) { int need target - nums[i]; if (mp.count(need)) { return {mp[need], i}; } mp[nums[i]] i; } return {}; }但如果题目额外强调数组是“有序的”那就可以用双指针从两端向中间逼近vectorint twoSumSorted(vectorint nums, int target) { int left 0, right nums.size() - 1; while (left right) { int sum nums[left] nums[right]; if (sum target) return {left, right}; else if (sum target) left; else right--; } return {}; }如果题目要求输出所有不重复的组合那还需要在找到一组解后跳过重复元素。这个扩展点也常被拿来追加提问。2.6 深度优先搜索岛屿数量这类题几乎是大厂笔试的“钉子户”。给一个二维网格1代表陆地0代表海水计算岛屿数量。本质上是求连通分量个数。标准DFS解法void dfs(vectorvectorchar grid, int i, int j) { int m grid.size(), n grid[0].size(); if (i 0 || i m || j 0 || j n || grid[i][j] 0) return; grid[i][j] 0; // 标记已访问 dfs(grid, i 1, j); dfs(grid, i - 1, j); dfs(grid, i, j 1); dfs(grid, i, j - 1); } int numIslands(vectorvectorchar grid) { if (grid.empty()) return 0; int count 0; for (int i 0; i (int)grid.size(); i) { for (int j 0; j (int)grid[0].size(); j) { if (grid[i][j] 1) { count; dfs(grid, i, j); } } } return count; }这里有一个关键细节标记已访问的方式。直接在原数组上把1改成0可以让空间复杂度降为O(1)但会修改输入数据。如果面试官追问“能不能不修改原数组”你可以引入一个visited二维数组。顺便说一句DFS在外面套双层for循环时循环里不要忘了判断grid[i][j] 1否则每个格子都进去一次DFS会TLE。这看起来是小事但笔试时越简单的粗心越致命。3. 实操过程从读题到提交的完整流程3.1 拿到一道题后先干这三件事在笔试的60到90分钟里你能不能稳定输出很大程度上取决于你的读题顺序和个人节奏。我自己的标准流程是固定的读题两遍第二遍开始用笔画输入输出样例理解计算过程。快速判断题目类型是模拟、贪心、DP还是图论。脑子里立刻和以前做过的题建立联系。先写框架函数签名、循环结构再填中间逻辑。这个过程看起来基础但能有效避免“看到题就开写写到一半发现思路错了”的惨剧。笔试和面试不一样没有面试官跟你互动你不能靠“问”来澄清题意。唯一的确认方式就是题目给的示例。如果你的思路连第一个示例都跑不通那就立刻停下来重新读题。3.2 用C还是PythonShopee笔试支持多种语言我自己更建议用C因为性能上限高而且很多公司的面试官默认你能熟练写C。但如果你Python熟用它也完全没问题。不过要敲黑板提醒不管用哪种语言都一定要事先熟悉该语言在笔试平台上的输入输出格式。C的cin/cout和scanf/printf混用时要小心Python的input()在大量读入时不要用sys.stdin.readline()以外的写法否则超时到想哭。3.3 必备的调试技巧笔试不是不能用本地IDE但千万不能依赖IDE的调试器。笔试环境常常不允许打断点我建议平时就练“打印日志调试法”函数入口处打印输入参数。关键分支处打印中间状态。循环结束后打印结果。这样即使到了任何只有基础编辑器的环境你也能靠printf把bug揪出来。4. 常见问题与排查技巧实录4.1 数组越界最隐蔽的致命伤很多题要求返回下标或者长度一个常见的错误是循环条件里用了i n导致多访问了一个不存在的元素。这类错误在本地跑小数据时很难发现因为C越界访问不一定会立刻崩它只是读到了脏数据一旦到了笔试的大数据用例轻则答案错误重则直接RE。建议写for循环时统一用i n如果一定要用一定要想清楚为什么。4.2 整数溢出大厂笔试最爱埋的雷当数组元素范围是10^9目标值是2 * 10^9时两数相加就可能溢出int类型。用long long是保险的但要注意如果代码里写的是int sum nums[left] nums[right];即使后面赋给long long也没用溢出已经发生了。正确的写法是一开始就把类型定义成long long。4.3 栈空未判括号题的经典翻车点在括号匹配相关的题里弹出的前提是栈不为空。很多人写st.pop()前忘了判空导致运行时异常。虽然你在草稿纸上推演时不会犯但一旦紧张手一快就写出来了。我的建议是凡是从容器中取元素之前养成先判断容器是否为空的习惯。4.4 数据规模没看清复杂度崩塌有些题的数据范围是n 100这时候O(n^3)也是能过的。但有些题数据范围是n 10^5O(n^2)直接TLE。我的建议是拿到题先看数据范围估算自己算法的时间复杂度在最大数据量下是否能在1秒内跑完。这里给一个速查表O(n) 能处理大约 10^7 到 10^8 的数据量O(n log n) 能处理大约 10^6 到 10^7 的数据量O(n^2) 在 10^4 以上就可能吃力O(n^3) 基本只适用于 10^2 到 10^3 的场景4.5 样例过了但提交0分边界条件没测很多同学说“我本地跑示例没问题啊”但提交就是0分。原因通常是只验证了正例没有验证边界空数组或数组长度为1输入全是相同元素目标值极大或极小字符串只有一种括号我个人的习惯是写完题后至少构造三组自测数据一组常规、一组边界、一组极端。这组动作只需要两分钟但能救回不少分。5. 高效的刷题方法与笔试策略5.1 刷题优先级时间有限的前提下不建议三百题一千题盲目刷。按这个优先级来字符串处理reverse、split、括号匹配、最长公共前缀。双指针有序数组两数之和、三数之和、最长无重复子串。二叉树前中后序遍历、层序、最大深度、最近公共祖先。贪心区间调度、跳跃游戏、分发饼干。动态规划斐波那契、爬楼梯、背包、最长递增子序列。图论基础岛屿数量、拓扑排序、Dijkstra。这套题覆盖的知识点就是这么面。当你把这几类刷熟后再去做大厂校招真题会发现大部分题都能归入这些框架里。5.2 错题本比刷题量更重要我用的是电子表格每次做错一道题就记一行包含题目名称、考察知识点、错误原因、正确思路一句话总结、类似题链接。过一周再回来做一遍错题。这个过程比刷十道新题都有效。因为人的记忆会遗忘尤其对题目套路和思维盲区隔几天回顾一次沉淀效果最好。5.3 笔试时的答题顺序如果笔试平台能看到所有题先花两分钟把全部题目浏览一遍按过题难度排个序。先把最有把握的题做掉保证有分到手再去啃难题。不要在一道题上死磕超过30分钟。笔试分数是所有题加起来算的一道题拿满分也无非那么点分不如把时间花在两三道中等题上。5.4 代码风格对隐形分的影响一些笔试平台有“面试官回看代码”的环节。如果你是面试官看到一份变量名乱起、逻辑堆成一坨的代码第一印象就不会太好。相反如果你的代码分层清晰、注释精准即使某道题没完全跑过面试官也可能给你一个“思路分”。6. 从一个真题答案到完整项目把算法题做厚最后我想说一个更重要的思路。不要满足于“题刷完了代码能跑出来”。真正的成长在于把一道真题延伸成一个小项目深入理解它背后的业务模型。比如做完“最长合法括号子串”后你可以想想购物网站上的优惠券校验系统。用户输入一串优惠码系统需要判断组合是否合法并要求连续合法段长度。这不就是括号匹配的业务化版本吗再比如做完岛屿数量后想想电商仓储里的库存热力图每个格子代表一个货架区域标记1的是需要补货的区域标记0是不需要的。多个相邻的1区域构成一个“补货连通块”计算补货连通块个数。又是一道一模一样的题。当你能把题面里的抽象数据结构和真实业务场景结合起来你的解题能力才算真正长在了自己身上而不是停在背题阶段。2019年校招季过去很久了但这类题的命题思路到今天依然没有过时。大厂笔试永远在变着法子考同样的底层能力数据抽象能力、算法应用能力和边界分析能力。把这些基础打扎实不管题库怎么更新你都能从容应对。
返回列表