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]); List<int[]> 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; Map<Character, 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]; List<List<Integer>> 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]); } Queue<Integer> 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的解法,就是好解法。