1. 为什么我把“模拟题”当成面试保底分
先说个挺扎心的现象:很多人刷力扣,一上来就扎进动态规划、图论这些看起来很唬人的标签里,结果刷了两个月,一到面试问个“中等偏下”的题目还是卡壳。反而是那些我一开始看不上的模拟题,最后成了我稳拿Offer的关键。
模拟题在力扣里的官方定义其实很朴素:不依赖某种特定算法技巧,主要考你能不能把题目的描述老老实实翻译成代码。翻译得准不准、边界处理得全不全、效率能不能过,这三个维度基本决定了一道模拟题你拿多少分。很多人觉得模拟题“简单”“没含金量”,但我知道不少面试挂在第2轮的人,其实不是不会难题,而是简单题写得漏洞百出。
适合什么人看这篇?如果你是刚开始刷力扣、不知道从哪里下手的萌新,或者刷了大几十题但总觉得代码写不干净的老手,这篇文章都能给你一些可落地的东西。我会从题型分类、刷题方法、真实踩坑、复盘技巧这几个角度把“模拟题”这件事讲透,全程用我自己的实际体感说话,不整虚的。
提示:力扣编号用的是LeetCode题库,下文提到题号均对应力扣(LeetCode)题目,方便你照着搜。
2. 模拟题到底考的是什么能力
2.1 拆解题意的能力
模拟题最关键的一步,不是写代码,而是读题。很多人拿到一道模拟题,第一反应是“这我好像会”,然后直接开写,写一半发现理解错了。我自己的经验是,模拟题的题干里藏着三类信息:
- 输入范围和多组输入的处理方式
- 边界状态,比如空数组、单个元素、最大值
- 操作顺序,题目描述的流程哪个先哪个后
拿力扣59题螺旋矩阵II来说,题干就一句话“给你一个正整数n,生成一个包含1到n^2所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵”。好多人一看“螺旋”两个字,就去想什么高深技巧。其实这道题的本质就是“你按东南西北的顺序一圈一圈填数”,纯粹考你能不能把“走格子”这个动作模拟清楚。
我举个例子,n=3,结果矩阵应该是:
1 2 3 8 9 4 7 6 5怎么实现?核心就是维护四个边界:上、下、左、右。先往右填到右边界,然后上边界下移一行;再往下填到下边界,然后右边界左移一列;再往左填到左边界,然后下边界上移一行;再往上填到上边界,然后左边界右移一列。这个循环直到所有数填完。
很多人卡在“什么时候该改变方向”,其实不用判断方向,你只需要在每一轮循环里固定做“从左到右、从上到下、从右到左、从下到上”这四步,每一步做完就缩边界。边界交叉了就结束。这就是模拟题典型的解题思路:不要试图找数学规律,严格照着操作流程走。
2.2 状态维护与细节控制能力
模拟题第二个核心考点是状态维护。想象你是个工厂流水线的质检员,手里有一堆零件,你得时刻知道现在加工到第几个、当前这个零件处于什么状态。代码里对应的就是变量:当前指针位置、当前方向、当前步数、当前剩余次数等。
力扣6题Z字形变换就是个好例子。题目要求把字符串按给定的行数从上到下、再从下到上地排列,最后从左到右逐行读取。这里最直白的模拟方法就是:用一个指针表示当前行,用一个变量记录方向是往下走还是往上走。走到第0行就改为向下,走到最后一行就改为向上。就这么简单,没有任何公式。
我记得有一次我帮人review代码,他写Z字形变换,用了很复杂的数学推导,最后还漏了行数为1的情况。我说你这代码我看了5分钟才看懂,面试官大概率没这个耐心。模拟题的代码应该是“一眼就能看懂”的,因为你本质就是在翻译流程,翻译得越直白越好。
2.3 边界条件的敏感性
边界条件这件事我必须单独拿出来说,因为模拟题丢分有六七成是丢在边界上。常见边界有这么几类:
- 输入为0、负数、空字符串
- 数组长度为1
- 数值达到int上限
- 循环边界是小于还是小于等于
力扣48题旋转图像,要求原地顺时针旋转90度。最简单的做法是两次操作:先按主对角线翻转(i和j互换,也就是转置),再左右翻转(每一行做reverse)。但如果你不记得“转置后行内反转”这个流程,你去硬推每个元素的新位置,很容易在边界上算错。
再比如力扣8题字符串转换整数,这道题看起来是正则表达式和字符串处理的活儿,实际上考的是状态机:跳过前导空格、判断正负号、读数字、判断溢出。每一步都有明确的状态转移条件,多一步少一步都不行。
我平时刷模拟题有个习惯:写代码前先在注释里把边界条件列出来。比如“空数组直接返回”“只有一个字符时直接返回本身”“溢出时返回INT_MAX或INT_MIN”。这样写出来的代码第一版通过率明显高很多,不信你试试。
3. 高效刷模拟题的实操方法
3.1 怎么选题:从基础题到热题100
模拟题数量很多,但不需要全刷。我自己的选题策略分三层:
第一层是入门打底,必刷这些:
- 力扣66题加一
- 力扣67题二进制求和
- 力扣415题字符串相加
- 力扣989题数组形式的整数加法
- 力扣9题回文数
这几道题都属于“高精度加法”家族,本质上是模拟小学数学的竖式加法。做完这五道,你对模拟题里的“进位”这个核心状态基本能形成肌肉记忆。
第二层是进阶巩固,重点突破“数据结构模拟”:
- 力扣232题用栈实现队列
- 力扣225题用队列实现栈
- 力扣622题设计循环队列
- 力扣146题LRU缓存(偏难,但模拟思想非常典型)
这类题目考的是你能否用已知的数据结构去模拟另一种数据结构的操作逻辑。我做232题时最大的感悟是:用两个栈模拟队列,入队直接塞进s1,出队时如果s2为空,就把s1全部倒进s2再弹出。整个过程没有任何奇技淫巧,但很多人就是绕不过“为什么要倒一次”这个弯。其实用生活类比就很好懂:栈像一口井,后倒进去的东西先被捞上来;你想让先倒进去的东西先出来,就得先把井里的东西全倒进另一口井里,顺序自然就正过来了。
第三层是冲刺拔高,对标力扣热题100里的模拟题:
- 力扣54题螺旋矩阵(59题的“读取版”)
- 力扣31题下一个排列
- 力扣38题外观数列
- 力扣43题字符串相乘
- 力扣48题旋转图像
这些题在面试中出现的频率很高,而且它们往往不单独出现,会作为某道难题的某个步骤。比如旋转图像那种转置加翻转的思路,在很多矩阵题里都能复用。
3.2 刷题的标准流程
我给自己定了一个“三遍法”,对付模拟题特别好用。
第一遍:完全不看题解,把题目翻译成伪代码。这个过程不用写完整代码,而是用中文把流程写清楚。比如“从左上角开始,按右、下、左、上的顺序走,走过的地方标记为已访问,走到尽头就换方向”。伪代码能写出来,说明你读懂了题;写不出来,说明有地方没想清楚,这时候去看题解效率是最高的。
第二遍:照着伪代码写真实代码,写完先自己造几个测试用例跑一遍,别急着提交。造用例一定要包含边界。比如n=1、n=2、空数组、长度为1的字符串,这些看起来很“弱智”的用例,恰恰最能发现问题。
第三遍:提交通过后,去看官方题解和高票答案,对比思路差异。模拟题往往有多种“模拟方式”,比如螺旋矩阵有人用边界收缩,有人用方向数组加visited标记。两种都能过,但面试时边界收缩明显更简洁,因为少了一个visited数组的额外空间。
我见过不少刷题博主强调“每天刷十道”,我不太认同这个量。模拟题的精髓不是量,而是“一次写对”。你刷十道每道都改半天,不如精刷三道一次提交通过。面试的时候没有提交按钮,没有编译器提示,一次写对的能力就是模拟题训练的终极目标。
3.3 时间复杂度和空间复杂度的基本意识
模拟题虽然不需要复杂的算法推导,但复杂度分析依然重要。说白了,模拟题就是“时间复杂度O(n)或O(n²)”的活:你要让面试官相信,你的代码在给定输入范围下跑得完。
举个实际例子:力扣43题字符串相乘,最常见的方法是“竖式乘法模拟”:每一位相乘,累加到对应位置,最后处理进位。时间复杂度是O(m*n),m和n是两个字符串的长度。这个复杂度在面试中是合格的,因为字符串长度通常不会上万。
但如果你写出一个“先转成整数再乘”的解法,比如直接int(a) * int(b),那就完全失去意义了——题目限制字符串长度可能到200位,转成整数必然溢出,这种解法连测试都过不了。模拟题的价值恰恰在这里:它逼着你在“结果可能超大数据范围”的前提下,老老实实用更基础的方式处理问题。
还有一个我常提醒自己的点:模拟题里多用循环、少用递归。递归写起来简洁,但在模拟“操作次数较多”的流程时容易栈溢出,代码可读性也下降。面试官看递归版的螺旋矩阵,大概率眉头一皱。我自己刷模拟题的原则是:能用迭代绝不用递归,除非递归深度很浅(比如二叉树那种)。
4. 典型模拟题案例拆解
4.1 案例一:力扣54题螺旋矩阵
为什么拿这个当第一个案例,因为它是模拟题里“方向感最强”的题。给你一个m行n列的矩阵,按顺时针螺旋顺序返回所有元素。
我直接放我经过多次调整后觉得最好懂的写法:
def spiralOrder(self, matrix): res = [] if not matrix: return res top, bottom = 0, len(matrix) - 1 left, right = 0, len(matrix[0]) - 1 while top <= bottom and left <= right: # 从左到右遍历上边界 for j in range(left, right + 1): res.append(matrix[top][j]) top += 1 # 从上到下遍历右边界 for i in range(top, bottom + 1): res.append(matrix[i][right]) right -= 1 if top <= bottom: # 从右到左遍历下边界 for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 if left <= right: # 从下到上遍历左边界 for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res注意我中间加了两个if top<=bottom和if left<=right的判断,这是很多第一次写这道题的人容易忽略的。不加这两个判断,当矩阵是长条形(比如3行1列)时,下边界和左边界会在不该遍历的时候被重复访问,导致结果多出元素。
这体现了模拟题一个很重要的习惯:在每一段“方向操作”之前,先问自己“这个方向还有没有元素可走”。你没有必要一次性把四个方向都走完,每个方向走之前检查一下边界是否仍然有效,是最稳妥的。
4.2 案例二:力扣31题下一个排列
这道题在模拟题里属于“规则模拟”的代表:题目告诉你要找字典序中下一个更大的排列,如果没有就重排为最小。难点在于你不仅要实现流程,还得理解流程里每一步的具体操作逻辑。
我的个人理解是分三步走:第一步从右往左找第一个下降点(nums[i]<nums[i+1]),这一步的目的是找到“可以变大的最靠右的位置”;第二步在i右侧从右往左找第一个比nums[i]大的数,与它交换;第三步将i右侧全部反转,让这一侧变成升序,也就是最小排列。
这个规则单独看每步都不难,但合在一起就有很多人绕不清“为什么要找下降点”。我举一个具体例子:[1,2,7,4,3]。从右往左看,3<4?不,4<7?不,7>2?不对,应该是2<7,所以i=1,数字是2。然后在[7,4,3]里从右往左找第一个比2大的,是3,交换得到[1,3,7,4,2]。最后把[7,4,2]反转成[2,4,7],结果是[1,3,2,4,7]。
看到没,整个过程就是你手写“找下一个排列”时的自然思路,代码只是把你脑中的动作一步一步翻译出来。很多人觉得这道题难是因为网上的解法直接背代码,不理解每一步的语义。我强烈建议你把这三步用一张纸画一遍,彻底理解了再动手写。
4.3 案例三:力扣38题外观数列
外观数列是“逐项生成”类模拟题的典型。题意是:从“1”开始,每一项都是对前一项的“读法”。比如前一项“1211”,读作“1个1、1个2、2个1”,所以下一项是“111221”。
这道题的精髓在于循环嵌套:外层循环生成第n项,内层循环扫描当前字符串并按连续相同字符分组。很多人一眼看完觉得很简单,真写起来又出问题。问题出在哪里?出在分组逻辑不熟练。
我最开始写的版本是这样:
def countAndSay(self, n): res = "1" for i in range(1, n): cur = "" j = 0 while j < len(res): count = 1 while j + count < len(res) and res[j + count] == res[j]: count += 1 cur += str(count) + res[j] j += count res = cur return res核心只有两行逻辑:统计连续相同字符的数量;把数量+字符拼接到新字符串。这不涉及任何算法知识,纯粹就是你能不能把“读前一项”的过程写出来。但就是因为逻辑简单,很多人反而不耐烦做,觉得“这不就是字符串拼接吗”,一上手却因为内层循环的索引边界出问题,要么越界,要么少读字符。
我做这道题的教训是:模拟题越简单,越要警惕“我以为我懂了”。把代码写出来、跑通、再回头审视每一行是否有多余或缺失,比急着冲下一题更有价值。
4.4 案例四:力扣66题加一与“高精度加法家族”
力扣66题太经典了:给定一个由整数组成的非空数组表示的非负整数,在该数的基础上加一,最高位数字在数组最前面。例如[1,2,3]表示123,加一返回[1,2,4]。
最容易想到的办法是:把数组转成整数,加一,再转回数组。但题目里数组长度可能上百位,转整数直接溢出。所以必须老老实实从最后一位开始模拟竖式加法:
def plusOne(self, digits): for i in range(len(digits)-1, -1, -1): if digits[i] < 9: digits[i] += 1 return digits digits[i] = 0 return [1] + digits这个解法只有5行,但信息密度极高:如果当前位小于9,加一结束直接返回;如果等于9,置为0并继续向前进位;如果循环结束了还没返回,说明全是9,比如[9,9,9],那就直接在开头插入1,结果是[1,0,0,0]。
我在给网友讲这道题的时候经常说:你不需要处理“如何进位”这个复杂问题,你只需要维护一个“是否继续进位”的状态。因为加一之后,每一位的操作只有两种可能:要么加完结束,要么变成0继续往前。抓住这个本质,代码自然就短了。
同样的思路可以做力扣67题二进制求和、力扣415题字符串相加、力扣989题数组形式的整数加法。这几道题做完,你对“从低位到高位”“维护进位状态”这两个模拟题核心考点会非常有感觉。
5. 四个高频踩坑点与排查思路
5.1 问题一:循环边界写错导致数组越界
这个我在螺旋矩阵那道题上踩过不止一次。常见症状是:本地跑得好好的,一提交报IndexError。排查思路很简单:把所有用到索引的地方列出来,手推一个极端用例。
比如螺旋矩阵,你用3行1列的矩阵手推一遍,看看在执行“从右到左遍历下边界”的时候,left和right是什么状态。我几乎可以确定,不加边界检查的话,第一次从右到左就会越界。
5.2 问题二:误以为模拟题不需要考虑效率
很多人觉得模拟题就是无脑写循环,完全不考虑时间复杂度。其实力扣的测试用例往往设计得很刁钻:比如外观数列第30项有多长?我告诉你,长度至少几千。字符串相乘的两个数长度可以到200位。虽然O(n²)能过,但你写个O(n³)的嵌套循环大概率会超时。
我的经验是:动手前先估算一下最坏情况。假设输入规模是10^5,你的双重循环是10^10,那基本没戏。模拟题虽然不需要你优化到极致,但复杂度至少要保证在数据范围下能跑完。
5.3 问题三:总想找数学公式替代模拟过程
这不一定是坏事,但对面试来说风险极高。力扣6题Z字形变换,网上确实有直接按行下标规律推的解法,时间复杂度O(n),比模拟的逐字填充更优雅。但问题是,面试现场你很难保证一次推对。我自己在面试时倾向于用更稳妥的方式,哪怕代码长一些,但每条逻辑都看得见摸得着。
话说回来,刷题阶段我建议你两种都写一遍。数学规律解法能帮你加深对问题的理解;模拟解法能保证你在任何情况下都有保底方案。两者都会,才是真的会。
5.4 问题四:没有养成良好的调试习惯
模拟题的代码通常比较长,变量也多,一旦出错很难靠眼睛找出来。我自己的调试流程是:先用print打印关键变量,追踪每一步的状态变化。比如螺旋矩阵,每走完一个方向就打印top、bottom、left、right以及当前结果数组;字符串相乘,每轮外层循环就打印当前的中间结果。
打印日志这种方法虽土但管用。等代码通过后再把print删掉就好。不要一上来就IDE断点,很多场景下过深的断点调试反而拖慢速度。
6. 我的复盘方法:从“刷过”到“掌握”
6.1 建立自己的模拟题笔记模板
我刷题记笔记有个固定模板,包含四项内容:核心考点(题目到底考了哪些模拟动作)、关键变量(需要维护哪些状态)、边界条件(哪些输入会让第一次写的代码挂掉)、优化空间(有没有更简洁的模拟方式)。
比如力扣48题旋转图像,我的笔记里记的是:核心考点是“两次翻转”——转置+行反转;关键变量是矩阵的行列索引i和j;边界条件是n为1时无需操作;优化空间是能否用原地操作避免额外数组。这个笔记看起来很简单,但如果面试前我快速翻一遍,基本就能回忆出代码怎么写。
6.2 同类题型横向归纳
模拟题的另一大价值是“一鱼多吃”。高精度加法家族、矩阵边界遍历家族、字符串逐项生成家族,每个家族都有一个共同状态需要处理。横向对比着刷,效果远好于随机乱序刷。
我自己习惯用一个表格记录同类题目:
| 题号 | 题目 | 核心模拟动作 | 容易出错点 |
|---|---|---|---|
| 54 | 螺旋矩阵 | 边界收缩+方向切换 | 长条矩阵时边界判断缺失 |
| 59 | 螺旋矩阵II | 边界收缩+填充计数 | 填充数量是否达到n^2 |
| 48 | 旋转图像 | 转置+行翻转 | 原地操作时的索引对应 |
| 6 | Z字形变换 | 行指针+方向标志 | 行数为1直接返回 |
| 38 | 外观数列 | 分组统计+逐项生成 | 内层循环索引边界 |
我建议你也建一张类似的表,刷完一个阶段就填一行。这张表的价值会在面试前集中复习时彻底释放:你不需要重刷全部题目,只需要看这张表,回忆每个题目的核心动作和易错点,就能快速恢复状态。
6.3 白板模拟:模拟题的最佳复习法
很多人做算法题复习是“重看一遍题解”,我的经验是“自己讲给自己听”。具体做法是:随机挑一道做过的模拟题,打开空白编辑器,不看任何参考代码,用注释先写中文思路,再逐行翻译成代码。
这个方法特别适合模拟题,因为它考察的就是“流程翻译能力”。如果连自己讲都讲不顺,说明这个题的流程你还没吃透。我平均一道题大概花15分钟做白板模拟,比在题单上划“已刷”有用得多。
7. 力扣热题100与OJ场景下的实战建议
7.1 如何在热题100中识别“模拟题”
热题100里并不是每道题都标签为“模拟”,不少模拟题被藏在别的标签下。我的识别方法就一招:看题目描述里有没有“按以下规则”“请你模拟”“依次执行”这类字眼。比如力扣43题字符串相乘,标签是“数学”“字符串”,但它本质就是模拟竖式乘法。力扣31题下一个排列,标签是“数组”“双指针”,但它的核心流程也是按规则模拟“寻找下一个更大排列”。
面试场景中,模拟题往往出现在第一轮的电面和笔试里,作为“筛选代码基本功”的题目。他不需要你掌握什么高级算法,只要你代码写得干净利落,边界不挂。所以我在准备面试时,会把这类题的优先级放在“滑动窗口”“二叉树遍历”之前。
7.2 OJ刷题时模拟题的定位
国内的OJ平台(比如牛客、洛谷)也有很多模拟题,但风格和力扣略有不同。力扣偏“工程化题意模拟”,OJ偏“过程模拟”,有些题目会要求你模拟比赛流程、日历计算、矩阵路径等。但底层的核心能力一致:能不能读一句做一句,把流程走通。
我建议刷OJ的朋友把力扣模拟题当成基本功训练场,再去OJ上挑战那些描述更长、步骤更多的综合模拟题。两者互相促进,一通百通。
7.3 面试现场做模拟题的节奏把控
最后聊一个很多人忽略的点:面试时模拟题怎么演。
先说代码书写顺序。我习惯于先写变量声明:需要几个边界、几个指针、几个标记位,第一步就列出来。然后是主体循环或主流程,最后补边界判断。这样面试官顺着你的代码走下来,会觉得你的思路非常清晰。
再说时间分配。模拟题如果5分钟内没理清流程,大概率是题目理解不到位,赶紧重新读题。宁可花5分钟把流程想透,也别急着写代码。代码一旦写完再改,反而更容易出乱子。
如果模拟过程中发现自己漏了某个边界,不要慌,直接说出来:“我在处理XX边界时发现需要加个判断,稍等,我补一下。”面试官看的是你发现问题和修正问题的能力,不是看你不犯错。
8. 一些私房经验总结
刷力扣模拟题这件事,看起来很简单,坚持做下去的人反而没那么多。很多人喜欢去啃硬骨头,觉得做难题才有成就感。但我想说的是:真正决定面试成绩的,往往不是你最难那道题写得怎么样,而是你是不是能在简单题上做到滴水不漏。
我个人练下来最大的变化是:代码一次通过率明显提升,边界条件几乎不再丢分,读题速度也快了。这些能力放到工作里同样受用,比如写SQL处理边界数据、写接口时处理异常输入,底层逻辑都是一样的。
如果你现在刷题刷得有点迷茫,不妨停一停,把力扣66题、54题、38题、31题这几道经典模拟题精刷一遍,按照我前面说的“三遍法”走:伪代码、真实代码、看题解对比。一套流程下来,你会对“模拟题”这三个字有完全不一样的感觉。
最后分享一个小技巧:刷模拟题时,每道题都问自己一句——如果面试官让我现场讲一遍流程,我能不能讲清楚?讲不清楚的地方,就是你还没吃透的地方,别放过它。明白了这个道理,再去刷任何模拟题,方向都不会错。