
2023年我把阿里系笔试中的“2星题”认认真真刷了三遍。老实说被这批题虐过之后我才意识到一个事实2星并不等于简单它在整套题目里扮演的角色恰恰是大多数候选人最该花时间打磨的区间。如果你正在准备2023年阿里巴巴编程题的在线笔试或面试手撕代码或者你只是想知道“2星题到底是什么水平”这篇文章就是为你准备的。我尽量把当时踩过的坑、总结出来的思考方式、还有最终沉淀下来的代码模板都写出来不绕弯子直接给干货。1. 2星题的真实定位为什么它比3星题更能反映代码基本功先聊一个很多人容易误解的地方星级的含义。阿里巴巴编程题一般按难度分成1星、2星、3星。1星题通常考基础语法和简单模拟很多不刷题的人也能写出答案3星题则往往涉及复杂的动态规划、高级数据结构、甚至多轮优化普通候选人半小时内很难完全吃透。2星题在我看来才是笔试真正的“分水岭”。为什么这么说因为2星题通常把一个经典算法模型藏在业务场景里面要求你在30到40分钟内读懂题意、抽象出模型、写出可运行的代码、并且处理掉各种边界情况。它不会故意为难你到需要构造冷门算法但也绝不会让你靠背模板就轻松搞定。它更像是在考察你是不是真的理解某个经典算法而不是只会套模板。另一个原因是在2023年的实际笔试中2星题占比非常稳定。整套试卷如果按分数权重来看2星题往往决定了你是否能进入下一轮。3星题做得再好前面2星题如果卡壳、超时、漏边界总分照样难看。反过来2星题稳定发挥加上1星题全对通过概率就相当可观。我自己的体会是刷2星题有一种“性价比”上的优势它的题型足够典型覆盖了笔试可能出现的大部分核心算法同时它的数据范围和题目结构又不像3星题那么刁钻适合用来建立稳定的解题节奏。所谓“稳定”不是说我每道题都会而是遇到一道题之后能快速判断它属于哪一类知道第一步该做什么、第二步该怎么做而不是在草稿纸上纠结半天。所以如果你只有两周到一个月的时间准备阿里巴巴编程题我的建议是先放弃难题把2星题做透。做透的标准不是AC了就算而是看到类似题目能闭着眼睛写出核心代码框架并且能说清楚每个步骤为什么会这样设计。2. 高频考点拆解2023年2星题在考什么把2023年出现过的2星题做一个横向对比可以发现考点非常集中。这里我整理了一张高频考点表格方便你快速对照考点方向出现频率典型呈现方式核心算法/数据结构数组与区间处理极高合并区间、求交集、区间覆盖排序、贪心、双指针字符串处理高最长无重复子串、字符计数、子串判断滑动窗口、哈希表动态规划入门高爬楼梯变形、打家劫舍、最大子数组和一维DP、滚动数组贪心策略中高跳跃游戏、加油站、任务调度贪心证明、排序图论入门中课程表、拓扑排序、连通分量BFS/DFS、入度表二分答案/查找中有序数组查找、最小值最大化二分模板、边界控制数学规律低中最大公约数、质因数、取模问题数论基础、快速幂从这张表能看出一个趋势2星题偏好“经典算法加一层业务包装”。比如不会直接考“合并区间”而是说“你有若干个会议时间请把重叠的会议合并成一个时段”。再比如不会直接考“拓扑排序”而是说“有若干个课程依赖关系请判断是否可能完成全部课程”。这种包装本身也是一种考察点。你需要在读题的时候把业务描述翻译成数据结构。翻译能力不强的人看到“会议时间”只会想到二维数组看到“课程依赖”只会想到嵌套循环而熟练的人一眼就知道二维数组排序后合并或者建图看入度。这个差距靠刷题是真的能补上的。另外我注意到2023年的2星题在数据规模上给了更大的宽容度。很多题目O(n log n)就可以过甚至部分O(n²)在优化常数后也能过。这其实是在暗示出题人更希望看到你掌握正确的基础算法而不是挖空心思去搞奇技淫巧。所以备考时不要追求“花式技巧”把基础模板写对、写稳反而是最有价值的策略。3. 四类核心题型的通用解法和代码模板这一部分我会写得比较细因为这是整篇内容里最可以直接“抄作业”的地方。每一类题型我都会给一个我在实际刷题时沉淀下来的模板并解释为什么这样写。3.1 区间问题先排序再贪心减少一半复杂度区间问题是2星题里的大热门。它最常见的形态是给出一组区间把重叠的区间合并或者判断区间之间是否有重叠。我第一次做这类题时第一反应是两两比较结果把自己绕晕了。后来才总结出核心思路区间题90%的情况先按区间起点排序剩下的事情就变得非常机械。以合并区间为例public int[][] merge(int[][] intervals) { if (intervals null || intervals.length 0) { return new int[0][2]; } // 按区间起点升序排序 Arrays.sort(intervals, (a, b) - a[0] - b[0]); Listint[] merged new ArrayList(); int left intervals[0][0]; int right intervals[0][1]; for (int i 1; i intervals.length; i) { if (intervals[i][0] right) { // 有重叠扩展右边界 right Math.max(right, intervals[i][1]); } else { // 无重叠保存当前区间更新为新区间 merged.add(new int[]{left, right}); left intervals[i][0]; right intervals[i][1]; } } merged.add(new int[]{left, right}); return merged.toArray(new int[merged.size()][]); }这段代码看起来简单但有两个关键点值得展开。第一为什么排序因为区间是否重叠本质上是在一维数轴上判断两个线段有没有交集。如果不排序你就需要O(n²)两两比较排序之后只需要线性扫描一遍因为左边区间的起点已经有序你只需要维护当前合并区间的右边界即可。这个“排序贪心”的组合把复杂度从O(n²)降到O(n log n)是区间类题目的核心套路。第二右边界为什么要取max而不是直接覆盖因为可能出现一个区间完全包含在另一个区间内的情况。比如[1, 5]和[2, 3]按顺序处理时如果直接把right改成当前区间的右端点就会把[1, 5]错误地缩短成[1, 3]。取max才能保证右边界永远是当前合并区间的最右端。如果你遇到的是“判断是否存在重叠区间”套路更简单排序后检查当前区间的起点是否小于上一个区间的终点如果是就说明存在重叠。这种题往往不需要真的合并只需要返回true/false千万别把merge完整写出来浪费时间。3.2 字符串与滑动窗口用“右扩左缩”控制状态字符串处理在2星题里的出镜率也非常高尤其喜欢考“子串”相关的问题。常用的工具是滑动窗口它天然适合解决“找连续子串满足某个条件”的题目。以经典的“最长无重复字符子串”为例我把它当作滑动窗口的入门模板public int lengthOfLongestSubstring(String s) { if (s null || s.length() 0) { return 0; } int left 0; int maxLen 0; MapCharacter, Integer map new HashMap(); for (int right 0; right s.length(); right) { char c s.charAt(right); if (map.containsKey(c)) { // 如果重复字符在窗口内移动左边界 left Math.max(left, map.get(c) 1); } map.put(c, right); maxLen Math.max(maxLen, right - left 1); } return maxLen; }这个模板的精髓在于“右指针不断扩左指针按需缩”。右指针每扫过一个字符就把它的最新位置更新到哈希表里。如果发现当前字符之前出现过并且上次出现的位置还在窗口内就说明窗口里有重复字符于是把左边界移动到“重复字符上一次出现位置的下一位”。这里有一个容易被忽略的细节为什么更新左边界时要取max因为左边界只能向右移动不能回退。如果直接写成left map.get(c) 1在遇到之前的重复字符已经在窗口左侧之外时可能会把左边界拉回左侧造成错误。取max就是在保证左边界是单调递增的。2023年的字符串类2星题里有一个比较典型的变体不是找最长无重复子串而是找“包含指定字符集的最短子串”。这种题本质上是同一套滑动窗口思路只不过需要额外维护一个字符命中次数的计数器。核心还是“右扩左缩”只是在左缩的时候要判断当前窗口是否仍然满足条件。这类题目一旦想明白做起来会非常快。3.3 一维动态规划从斐波那契到“打家劫舍”变形动态规划是2星题里让很多人头疼的部分。其实2星题的动态规划并不复杂绝大多数都是“一维DP”即状态只跟前面的一个或两个状态有关。关键在于你能不能从题目描述里看出“这个题可以用DP”。我建议先把“打家劫舍”这个经典题吃透因为它背后隐藏的是一大类“相邻互斥”问题public int rob(int[] nums) { if (nums null || nums.length 0) { return 0; } if (nums.length 1) { return nums[0]; } int pre2 0; // 前前一个位置能获得的最大金额 int pre1 0; // 前一个位置能获得的最大金额 for (int num : nums) { int cur Math.max(pre1, pre2 num); pre2 pre1; pre1 cur; } return pre1; }这个代码的精妙之处在于它只用两个变量就完成了DP数组的滚动。pre2表示“不取当前元素时前面积累的最大值”pre1表示“取到前一个元素为止的最大值”。当前元素有两种选择不取它那么结果等于pre1取它那么结果等于pre2 num。两者取较大值就是当前位置的最优值。很多人第一次学DP时会觉得“为什么状态转移是这么定义的”其实可以这样理解对于第i间房子在循环中就是当前num我只有“抢”和“不抢”两种决策。抢的话第i-1间不能抢所以能得到的最大收益是dp[i-2] nums[i]不抢的话那么前i-1间的收益就是dp[i-1]。二者取最大就是dp[i]。这个过程写完你就掌握了动态规划最核心的思维把大问题拆成“当前这一步的选择 之前子问题的最优解”。2023年2星题里出现过的动态规划变体比如“打家劫舍II”围成一圈、“删除并获得点数”、“爬楼梯的最小代价”其实都是在这个模板上做调整。围成一圈时只需要拆成两个直线问题不抢第一家或者不抢最后一家分别跑一遍直线版本再取最大值。这个思路我当时在考场上想了好一会儿才明白其实本质上还是“把环形拆成线性”属于非常经典的DP优化技巧。3.4 图与拓扑排序不建图也能做的入门级题目图论在2星题里一般不考特别复杂的最短路而是偏好“拓扑排序”和“连通性判断”。这类题目有一个共性你不需要把整个图结构建得很复杂只需要维护入度、邻接表然后从入度为0的节点开始BFS。以“课程表”为例即给定课程数和依赖关系判断是否可能完成所有课程public boolean canFinish(int numCourses, int[][] prerequisites) { int[] inDegree new int[numCourses]; ListListInteger adj new ArrayList(); for (int i 0; i numCourses; i) { adj.add(new ArrayList()); } for (int[] pre : prerequisites) { inDegree[pre[0]]; adj.get(pre[1]).add(pre[0]); } QueueInteger queue new LinkedList(); for (int i 0; i numCourses; i) { if (inDegree[i] 0) { queue.offer(i); } } int cnt 0; while (!queue.isEmpty()) { int cur queue.poll(); cnt; for (int next : adj.get(cur)) { inDegree[next]--; if (inDegree[next] 0) { queue.offer(next); } } } return cnt numCourses; }拓扑排序的核心原理是每次找出一个“当前没有前置依赖”的节点把它处理掉然后更新它所有后继节点的入度。如果最后处理过的节点数等于总节点数说明图中没有环所有课程都可以完成如果少于总数说明存在环某些课程永远无法满足前置条件。这个模板里最需要牢记的是入度的定义是有多少条边指向当前节点。当某个节点的入度变为0意味着它的所有前置课程都已经完成可以入队学习了。我一开始经常搞反方向把入度建成了出度导致结果莫名其妙。后来专门总结了一句话先数前置再找零入度。如果题目要求输出一种“可行的学习顺序”其实只需要把queue.poll()出来的节点按顺序收集进结果数组即可。这是从“判断是否存在”到“构造方案”的升级思路完全一样只是多了一个记录步骤的数组。4. 边界条件和性能陷阱笔试挂掉最常见的三个原因代码模板都背熟了笔试还是可能挂而且挂在让人意想不到的地方。下面这三个问题是我和身边朋友在2023年笔试中真实遇到过的也是最容易丢分的点。4.1 空数组和极端小规模输入很多人在本地IDE跑得好好的一提交就报错原因是没处理空数组、空字符串、只有1个元素的情况。比如区间合并题目如果输入是intervals.length 0你的代码如果直接访问intervals[0]就会报数组越界。我见过很多同学在笔试中因为这种低级错误丢分非常可惜。解决的唯一办法是在写完代码之后立刻在脑子里过一遍边界用例空输入、单元素输入、两个相同输入、数据全是相同值。这几类用例不需要运行只要在代码里主动判断就能挡掉80%以上的边界错误。4.2 int溢出问题2星题虽然数据范围不算变态但某些题目确实会诱导你直接使用int。比如计算最大子数组和、区间累加、斐波那契序列都可能在中间过程中超过int范围。一旦溢出答案就会变成负数或者奇怪的大数导致判断逻辑完全错误。所以看到一个题目里说“n 10^9”或者“数组元素 10^5”时不要急着认定int够用先估算一下最坏情况如果累加所有元素会不会超过2^31-1。如果会就果断用long。我在刷题时养成一个习惯凡是涉及累加、相乘或者比较大小的DP题目优先声明成long后面即使不需要也不会出错。4.3 超时但不知道优化方向有些题目本地测试数据很小看起来O(n²)也能过但笔试后台很可能有大数据测试。2023年的2星题里有个典型场景给定一个数组求每个元素右边第一个比它大的元素位置很多人的第一反应是双重循环。如果是2星题这种暴力解法通常只能过部分用例超时是必然的。正确的做法是想到单调栈。单调栈的核心是维护一个严格递减的栈在遍历过程中如果当前元素比栈顶元素大就说明当前元素就是栈顶元素的“右边第一个更大值”于是弹出栈顶并记录答案。这样每个元素最多进栈一次、出栈一次时间复杂度是O(n)。把暴力O(n²)优化成O(n)往往就是2星题和3星题之间的那条线。我建议在备考时把“暴力解法”作为第一步但只用于验证思路不要作为提交目标。写完暴力解法后再思考这个解法慢在哪里有没有重复计算能不能用空间换时间这个“暴力→优化”的过程做上十道题之后你会发现自己对数据结构的敏感度提升非常快。5. 针对2023年笔试的刷题节奏与实战策略最后聊一聊时间分配和备考节奏。我见过不少同学刷题很勤奋每天做七八道题但效果很差原因是“贪多嚼不烂”。准备2星题我更推荐“专题突破”的方式而不是天天换类型。我当时的时间安排大概是这样的你可以根据自己的基础调整时间段重点任务每日题量第1周数组、双指针、滑动窗口3-4道第2周排序、区间、贪心3-4道第3周一维DP、背包入门2-3道第4周图论、拓扑、二分2-3道考前3天回顾错题、重写代码模板按需这个节奏看起来不算快但要求是“每道题都吃透”。什么叫吃透第一能独立不看答案把代码写出来第二能解释清楚每个关键步骤为什么要那样做第三能在题目基础上自己造几个变形题想想如果条件改变解题方案要怎么调整。能做到这三点才算真的掌握了一道题。到了真正的笔试环节我有几个比较实用的策略想分享。第一先花1到2分钟快速浏览所有题目从最简单的题开始做。不要按顺序死磕尤其不要把时间全耗在某一题上。2星题通常有3到5道每道题分配的时间最好不超过25分钟。如果25分钟还没有思路立刻换下一题最后再回来补。第二写代码之前先想清楚整体结构。我见过很多同学拿到题就上手敲结果写了一半发现思路不对删掉重来白白浪费大量时间。最稳妥的做法是先在草稿纸上写下算法的核心思路比如“排序后贪心合并”“滑动窗口哈希表”再用几行伪代码描述主流程。确认没有逻辑漏洞后再开始写正式代码。第三写完代码之后别急着交。花30秒手动跑一个最小样例。比如区间合并手动跑一个[[1,3],[2,4],[5,7]]看看代码流程是不是符合预期。这样能抓住大量“变量写反”“边界条件漏判”的问题。第四如果实在做不出来也不要留空白。把暴力解法写上去至少可以过一部分测试用例拿到一部分分数。在笔试中部分分往往比你想的更重要它可能决定你是否能进入下一轮。我个人在刷完2023年2星题之后最大的收获不是会了某几个算法而是建立了一套“解题手感”式的东西看到一个题能自动判断出考点知道需要用哪种数据结构然后快速进入“模板变形”的编码状态。这种状态在真正的笔试中非常值钱因为时间紧、压力大你根本没有时间去慢慢推导一个全新算法能依赖的就是平时练出来的条件反射。如果你想把这篇文章沉淀的东西转化成自己的我建议你从今天开始挑一个考点方向比如“区间问题”然后连续练5道题。第一天做一道第二天做两道第三天做两道。你会发现到第三天你已经可以熟练地在看到题目的前10秒钟判断“这题是用排序贪心还是用差分数组”。这种感觉比记住任何代码模板都重要。最后再分享一个小小的经验准备2星题不要只盯着标准答案看。那个答案可能是最优解但未必是你在考场上最容易写对的解法。你完全可以用稍微笨一点但更不容易出错的做法。比如区间合并有人用差分数组也能做但排序贪心的思路更直观出错率更低。在笔试环境下能稳定AC的解法就是好解法。