搞算法题这件事,最尴尬的莫过于:题刷了不少,一面试又懵了。我更倾向于把刷题当成一种“脑内体操”,它不是背诵解法,而是刻意训练自己识别问题模式、搭建思路框架的反应速度。这篇文章不打算做成LeetCode题解目录,而是想聊聊我在刷“部分算法题”时沉淀下来的一套复盘框架:怎么选题、怎么切入、边界和复杂度怎么抠、怎么把一道题真正变成自己的。如果你正处在刷题瓶颈期,或者纯粹想换个思路重新审视算法学习,这篇应该能给你一些不一样的角度。
1. 选题和复盘:大部分人的刷题姿势从根上就错了
先问个问题:你刷题是顺着题号从1开始,还是按知识点分块刷?我见过太多人栽在第一种上。顺着题号刷,看似努力,实际上是在和出题顺序的随机性搏斗。今天碰一道链表,明天碰一道动态规划,大脑还没来得及建立模式识别,就被不同算法来回打断。这种“横向跳转”的刷法,训练的是零散记忆,不是结构化能力。
我的经验是:按题型或者算法范式垂直打穿。比如这周只搞滑动窗口,下周只搞岛屿类DFS/BFS,下个月集中回溯。因为算法题之间存在大量“母题”关系——一道中等难度的题,往往是某道hard题剔除了迷惑条件后的骨架。垂直刷能让你的大脑持续接受同一种抽象模式的刺激,更容易提炼出“这类题我拿到手应该先看什么”的反射链路。
选题之外,更关键的是复盘。我以前也以为复盘就是看官方题解,看懂就过,后来发现这是最大的错觉。看懂别人的思路,和你能独立把这条路走出来,中间隔着一整个“盲画流程图”的过程。真正的复盘至少要做三件事:
- 光看题解后合上,凭记忆在纸上还原核心循环和剪枝条件;
- 把解法里“哪个条件删掉会出问题”想明白,最好亲手注释掉跑一遍看错误长什么样;
- 过一周再用同一道题自测,看自己是真懂了还是只是眼熟。
这个过程听着麻烦,但它把一道题从“我看过”变成“我会推”。刷题数量唬不了人,能无提示写出来才算数。顺便说一句,复盘不需要写长篇大论,一张草稿纸、几个关键变量的注释就够了,重点是强迫自己重新推导一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 判题信号与暴力解的价值:先会跑再学飞
很多刚刷题的同学有个毛病:拿到题第一反应就是“这题要用什么算法”。恕我直言,这跟还没学会走就想跑一模一样。算法是工具,判断用什么工具的前提是你得知道问题本身长什么样。太早纠结算法标签,会被思维定势绑死。
我的习惯是,拿到一道题先问自己三个问题,不急着写代码:
- 输入规模有多大(这直接决定你能不能用O(n²)或更高复杂度的解法)?
- 有没有明显的“最值”“子串”“排列组合”这类语义信号?
- 如果我用最笨的办法暴力枚举,流程是什么?瓶颈卡在哪一步?
第三个问题特别重要。很多人瞧不上暴力解,但实际上暴力解是突破正规解的最佳跳板。举个典型的例子,求最长无重复字符的子串——暴力解法就是枚举所有起点,把窗口往右拉,遇到重复就停。写一遍暴力解之后你会发现,重复计算就藏在“每次枚举起点都重新走一遍窗口”这个动作里。一旦看穿瓶颈在于重复扫描,双指针/滑动窗口的思路几乎是自己蹦出来的。
暴力解的价值不只是通向正解,它还是你检查正解的“参照坐标”。你写完优化的代码,如果跑随机测试样例时结果和暴力解不一致,你能立刻知道自己哪里推错了。我甚至建议把暴力解当成验证器保留在本地注释里,面试时你可以告诉面试官:“我先用一个朴素思路确认正确性,再逐步优化时间空间。”这套思路展示的是工程思维,远比直接背出最优解更打动懂行的人。
说到底,大多数“难题”都是“中等题+一层伪装”而已。剥伪装的能力,恰恰来自你对朴素解抠得够不够细——那个不优雅但诚实的答案,才是所有优化思路的锚点。
2.1 信号识别:从题目用词反推考察点
我花了很久才总结出题目词汇和算法标签之间的对应关系。这不是玄学,是文字背后数学结构的映射。比如看到“连续”“子数组”“窗口”这类字眼,几乎可以锁定滑动窗口或前缀和;看到“最短路径”“连通性”就要往图论方向想;看到“所有可能组合”“排列”基本是回溯;看到“最大值/最小值且决策受前序影响”则在暗示动态规划。
为了更直观,我整理过一张自己用的对照表,每次刷题前瞟一眼,用来快速定位思考方向:
| 题目常见用词 | 优先怀疑方向 | 自检的问题 |
|---|---|---|
| 连续、子数组、窗口 | 滑动窗口/前缀和 | 窗口右移时左边界能否同步收缩 |
| 最长、最短、方案数 | 动态规划/贪心 | 是否存在重复子问题且局部最优可递推 |
| 组合、子集、全排列 | 回溯 | 如何剪掉重复分支 |
| 连通、岛屿、网格邻接 | DFS/BFS/并查集 | 访问标记写在哪一步最省事 |
| 第K大、前K个、topK | 堆/快速选择 | 数据规模是否允许全排序 |
这张表极大改善了我的读题效率。以前看到“连续子数组”四个字我脑子里一团浆糊,现在第一反应就是画滑动窗口的边界演进图。但必须提醒一句:这是“优先怀疑”,不是“定死答案”。表只是帮你找起点,具体还要靠测试用例去验证,千万别拿它当万能公式。
3. 核心技巧拆解:滑动窗口与回溯的实际打法
选定一个方向打穿之后,你会发现很多题其实是同一招的变式。这里挑两个最常被卡住的模型专门拆开讲:滑动窗口和回溯。别小看这两个,一个处理“节流”,一个处理“穷举”,它们的共性在于对边界和状态的掌控极其敏感,你的细节把控力基本决定了解法能不能一次通过。
3.1 滑动窗口:先搞懂窗口的语义再写循环
滑动窗口这个套路的核心不是“滑”这个动作,而是窗口在哪里、什么时候滑动、滑动时左侧还是右侧动。这三件事想不清楚,写出来的循环基本是错的。
我通常用一个所谓的“窗口不变式”来约束自己:窗口内部始终维护着满足题目条件的某种性质。比如无重复字符问题里,窗口内的字符永远互不相同。每次右指针吃进一个新字符时,如果破坏了不变式(出现了重复字符),就移动左指针直到不变式恢复。用不变式写代码,你可以完全不用记代码模板,只关注“这次操作会不会破坏性质”。
具体的伪代码骨架如下:
text复制left = 0
for right in range(n):
将 nums[right] 纳入窗口,更新窗口状态
while 窗口不再满足不变式:
将 nums[left] 移出窗口,更新窗口状态
left += 1
此时窗口内的状态满足要求,根据题目更新答案
注意while部分,很多人的bug就出在这:明明用if就够了却写成while,或者正好反过来。区别在于,移除一个左边元素后窗口性质是否可能依然不满足。如果可能,就必须用while持续收缩边界。一个很笨但有效的自测办法:构造一个极端输入,比如所有字符都相同,然后手动推演几步循环,看left是否最终能追上right。
写过几道滑动窗口题之后,你还会遇到一个升级问题:窗口状态的“撤销”比“加入”更容易写错。加入时你只管更新频率计数,撤销时却要同时处理“计数减为0要不要删除”这类细节。建议在草稿里把计数器的状态变化画成多行小表,一列是一步操作,这样复杂度瞬间下降。
3.2 回溯:把“撤销选择”当成一等公民
回溯的框架极其单调,难点从来不在框架本身,而在剪枝和去重。框架的核心是:做选择、递归、撤销选择,三步走。但我一开始写就范了低级错误——递归分支返回之后忘了撤销,导致下一分支的候选列表被上一分支污染。
回溯问题我现在的做法是画“决策树”。根节点是初始状态,每层代表一个位置的选择,叶子是完整结果。画完树再写代码,能直观看到我在哪个分支要排除哪些选项。比如组合去重问题,最常见的做法是先排序,再在for循环里检查“当前元素是否和前一个相同且前一个没被用过”,如果是就跳过。这里有个隐藏细节:两个“相同值”在当前层不能重复选,但在不同层可以分别选,所以剪枝条件必须放在“当前层迭代同一个位置”的语境下判断,不能单纯靠值相等就跳过。
回溯的性能问题是另一个深坑。很多回溯题的搜索空间是爆炸性的,不加剪枝可能跑半天。剪枝除了规范的去重条件外,还可以根据题目做可行性剪枝,比如剩余可选元素的数量都不够凑齐结果长度时,直接放弃分支。写这种剪枝时,务必在注释里写下“为什么可以安全剪掉”,否则过两个月你自己都看不懂当初这行代码的依据是什么。
还有一个我在实际中反复踩的点:结果收集的时机与引用复用。你以为你把结果加入答案了,却发现所有结果一模一样——因为加入的是同一个可变对象的引用,后续修改把它覆盖了。解决办法是每次加入答案时拷贝一份快照。这条坑几乎覆盖了所有回溯题,越早养成“拷贝再入队”的肌肉记忆,后面越省心。
4. 复杂度与边界:你的代码不是跑通就算完了
很多人刷题追求“Accepted”就收工,但真正的考核往往藏在“内存和时间到底占多少”这层。面试中面试官非常喜欢追问“时间复杂度是多少”“能不能优化到对数级”。要是你只会背结论说不出推导过程,印象分会大打折扣。
时间复杂度的分析不要靠“感觉”,要靠“看循环嵌套和递归深度”。双循环嵌套且每层都遍历n,基本就是O(n²);递归时每个节点访问常数次、递归深度为n的幂,那大概率是某种指数或对数公式。遇到分治、二分这类,多用“递归表达式”来推导,比如T(n)=2T(n/2)+O(n)这种结构,直接套主定理或自己展开算几层。
空间复杂度更容易被忽略。你以为只用了几个变量就是O(1),结果递归调用栈在最坏情况下的深度就是O(n),一估算全露馅。特别是快排递归、树DFS这类,深度不稳,最坏情况下可能吃满栈。碰到系统栈深度敏感的语言,还得多想一步“是否要改写成显式栈遍历”。
边界条件则是个细心活,也是Online Judge最常埋坑的地方。空输入、单元素输入、两个元素、全相同、全逆序、目标值在区间端点附近,这几类用例我每次写代码前会提前列在草稿纸上。测试不是等你写完才做,而是边写边脑补边界走查。再举个例子,求中位数的题,偶数长度时取两个中间值的平均,很多解法在“下标越界或除以零”的边缘反复横跳,就是因为在边界上少推了一步。
有个很实用的习惯:每道题写完,都强迫自己至少构造三个边界用例去跑一遍。哪怕最终答案全对,这个动作的收益也远大于多刷两三道简单题。因为边界用例训练的是你对“数据流动到极限状态时会发生什么”的敏感度——这在真实工程里,比任何花哨技巧都保命。
4.1 输出对比与随机验证法
如果你自己设计测试件,还有一招可以大幅提升信心:随机验证。用脚本生成随机合法输入,同时运行你的优化解和暴力解,对拍输出。只要随机量够大、范围覆盖够广,两者输出持续一致,你这个解法基本就稳了。这个方法花的时间很少,但对查漏补缺非常有效,尤其是动态规划和贪心类问题——因为思路一旦局部错误,常规样例根本测不出来。
text复制for 生成若干随机数据:
brute = 暴力求解(data)
optimized = 优化求解(data)
if brute != optimized:
打印 data,定位差异
在本地调试时强烈建议养成这个流程。它还能反过来帮你理解算法的适用条件——当随机数据命中“反例”时,你往往会发现自己的优化解法压根不满足某个隐藏前提。这比盯着题解死背要深刻得多。
5. 刷题时间管理:别让“题海”吃掉你的深度思考
刷题容易上瘾,因为每通过一道就有一个即时的正反馈,但正因为如此,很多人刷了几个月还在原地踏步——被简单题的成就感喂饱了,没剩多少精力去啃真正的硬骨头。我给自己定的规则是:每天不追求数量,只追求在深度和难度上跨一个台阶。宁可两小时只磨一道中难题,把边界、剪枝、复杂度全部过一遍,也不要一小时机械刷六道简单题。
我习惯用“番茄工作法”的改良版来安排:25分钟独立写题,不查任何资料,就当自己是面试现场;时间到还没思路,就允许自己看题解,但只许看完第一个核心提示就关掉,再继续独立写完。这个过程很折磨,但极其见效。它逼着你把“不会——看答案——记住”的循环,改造成“尝试——卡住——解锁关键提示——自己走完剩下的路”的主动学习模式。
另外,合理利用“错题回放”机制也很重要。我建了一个简单的清单文件,记录每个题号的思路关键词、踩坑点、下次复刷日期。每周固定抽一天只干一件事:复刷错题,不看题解重写。这比刷三道新题管用得多,因为遗忘曲线在算法学习里的杀伤力远超你的想象,你值得在它发挥作用前就设一道防线。
说到底,刷题时间管理的本质不是“挤时间”,而是“护住深度思考的时间”。算法能力的提升靠的是慢思考,不是快反馈。所以每当我发现自己开始疯狂扫简单题追求绿勾时,我就知道该停下来了,回到那道卡了我一夜的中等题去,把它彻底吞下去——那种吞下去的爽感,远比十个绿勾持久。
6. 三类实战场景的取舍与避坑心得
到了后期,你会发现自己刷题的目的开始分化:有人是为了面试突击,有人是为了比赛抢分,也有人单纯为了工程里遇到问题时能快速建模。目标不同,刷法就完全不同,选错赛道才是最大的隐性浪费。
面试导向的刷题策略应该是广度优先加中等题为主,覆盖常见题型和变式。难点在于快速读题、把自己会的那套解法讲清楚、再针对追问做优化。这个场景最忌讳的是在hard题上较劲,时间有限,性价比太低。你自己心里清楚,面试官要看到的不是你会不会那一道hard,而是你面对陌生问题时的拆解路径是不是清晰。
竞赛导向则恰恰相反,要练的是“套路库”的深度和临场用最优解的果断性。很多竞赛高手不是每道题都从零推,而是靠大量积累模型和模板,一看题就直接套,省掉摸索阶段。竞赛导向还有一个特点:代码实现的稳定性和容错性是重中之重,摊上一个隐藏的边界bug可能直接导致整题失分。所以竞赛党平时就应该训练自己读写代码的速度和一次写对的准确率。
工程导向又是另一片天地。工程里算法大多是嵌入式的小功能,比如海量日志里找频繁项、流式数据窗口统计、图结构的分层遍历。这类场景更在意算法的可维护性和增量更新能力,而非纯理论最优。在这种场景下积累的很多技巧——比如滑动窗口的“加一个走一个”循环、并查集的路径压缩打印——反而是面试中的加分素材,因为你能讲清楚“真正跑在生产环境里是什么表现”,而不是只会背文字定义。
三条赛道不是互斥的,但必须让现状目标决定你今天的投入方向。如果你现在的主要矛盾是找饭碗,就别被竞赛难题勾走魂;如果你在打省赛,就不要天天抠边界思维题。随时问自己一句:我现在最需要哪项能力?然后带着这个答案去选题。有了明确的行为标准,刷题就从一个情绪驱动的习惯,变成了真正工具化的技能建设。
