2026年刚开年那阵子,我给自己定了个小目标:把算法题的手感重新捡起来。说实话,工作之后写业务代码的时间多了,很多基础的数据结构和经典题型反而生疏了,碰到稍微绕一点的题就总想去看题解,这让我有点慌。尤其是看到不少人靠着系统刷题拿到了心仪的 offer,或者在代码质量上明显更稳,我就更觉得这件事值得认真做一次。所以 1 月 21 号到 22 号这两天,我把自己关在书房里,专门集中刷了两天题,顺便把过程中的思路、踩坑、时间分配全部记了下来,就是标题里这份“刷题笔记”。这篇笔记不适合打算三天速成的人,它更适合那些跟我一样,想稳扎稳打把算法基础补回来、并且真的愿意花时间做复盘的人。接下来我把这两天的完整过程、题型总结、调试教训和一些真实感受都写出来,希望能给你一些参考。
1. 刷题前的准备:目标、范围与节奏
1.1 为什么把目标定成“恢复手感”而不是“题海战术”
这次刷题之前,我认真想了一下自己的短板在哪里。如果只是漫无目的地每天刷几道题,看起来努力,实际上效率往往很低。因为刷题这件事,最关键的不是“量”,而是“题型识别”和“思路转换”。比如拿到一道题,你能不能第一时间判断它属于双指针、动态规划、回溯还是图论,这决定了后续的解题速度。所以我给这两天定的目标很简单:不追求一天写几十道,而是把高频的核心题型重新过一遍,每道题都做透,做完之后写出完整的思路复盘。
具体操作上,我把目标拆成了三层。第一层是基础数据结构:数组、链表、栈、队列、哈希表,这部分主要是恢复记忆力,刷一些经典题巩固一下 API 使用;第二层是高频算法思想:二分查找、双指针、滑动窗口、递归、回溯、基础动态规划,这部分是重头戏,花的时间最多;第三层是快速过一遍二叉树相关的问题,因为树的题目面试出现频率实在太高了,而且很多复杂问题都能化归到树的遍历上面。两天的时间,我给自己定的量是 20 道题左右,每一道都要求能独立完成,并且整理出笔记。
1.2 两天的题目范围与时间安排
我按照自己的精力曲线,把时间分成了几个小块。上午头脑清醒,适合啃动态规划和回溯这种需要深度思考的题目;下午稍微有点疲惫,主要做双指针、哈希表这类套路比较固定的题;晚上则用来做二叉树和整体复盘。这里我列一下我实际执行的时间表:
| 时间段 | 刷题主题 | 目标完成量 |
|---|---|---|
| 第一天上午 | 数组、哈希表、双指针 | 6 道 |
| 第一天下午 | 滑动窗口、前缀和 | 4 道 |
| 第一天晚上 | 二叉树遍历与递归 | 4 道 |
| 第二天上午 | 动态规划入门(背包、爬楼梯) | 4 道 |
| 第二天下午 | 回溯算法(组合、排列、子集) | 4 道 |
| 第二天晚上 | 错题复盘与笔记整理 | 整理全部记录 |
这种安排最大的好处是,每个时间段都有一个相对聚焦的主题,大脑不需要频繁切换思维模式,进入状态会更快。很多刷题新手容易犯的一个错误是“一天之内什么题都刷”,比如刚做完一个链表题,马上又跳到一个图论题,结果每道题都刚开了个头就换了方向,第二天回头一看全忘光了。我用的这种主题式刷法,某种程度上相当于做了“批量练习”,同一类型的题连续做几道以后,套路感会非常明显,进步也会更快。
2. 高频题型的套路拆解与实操解析
2.1 双指针与滑动窗口:从暴力到 O(n) 的思维转变
第一天上午我先刷了双指针。说实话,双指针本身不是一个特定的数据结构,它是一种优化思路。很多数组、字符串相关的题目,暴力的做法是两层循环枚举,复杂度 O(n^2),但当数据范围超过 10^4 的时候,O(n^2) 往往就会超时。双指针的核心思想是:利用数组的有序性或者某种单调性,让两个指针分别从两端或者同向移动,在一次遍历的过程中完成计算。
我拿“三数之和”这道经典题举例。暴力做法需要三层循环枚举 i、j、k,结果不仅要处理超时,还得费劲去重。用双指针之后,思路变成了:先把数组排序,然后固定一个数 nums[i],再用一左一右两个指针去查找剩下两个数。左指针从 i+1 开始,右指针从末尾开始,如果三数之和大于 0,说明右边的数太大了,右指针左移;如果小于 0,说明左边的数太小了,左指针右移;等于 0 就记录答案。排序的复杂度是 O(n log n),双指针部分因为是固定一个点再扫一轮,整体是 O(n^2),但比三层循环还是要稳定很多,而且去重逻辑也更清晰。这道题我印象很深的地方在于去重。很多人写出来功能是对的,但输出结果里有重复的三元组,原因就是没有在找到一个答案之后,主动跳过重复元素。这个细节,只有在手写一遍、看到错误输出之后,才能真正记住。
滑动窗口和双指针的关系非常紧密,我下午刷的“无重复字符的最长子串”就是典型代表。这道题维护一个窗口,右指针不断向右扩展,把新字符加入窗口;左指针则根据情况收缩,保证窗口内没有重复字符。这里的关键是,你用哈希表记录每个字符最后出现的位置,当遇到重复字符时,直接把左指针跳到 max(左指针, 上次出现位置+1),而不是老老实实一步一步挪。我在实际写代码的时候,第一次漏掉了取最大值这一步,结果在某些用例上左指针会往回退,直接导致结果偏大。后来调试时打印出每一步的左右指针才发现了问题。这个细节,其实就是滑动窗口题里最容易出错的地方。
2.2 动态规划:状态定义比转移方程更重要
第二天的重头戏是动态规划。学 DP 的时候,很多人第一反应是去背“背包九讲”里的转移方程,但我自己的体会是,如果你能清楚地回答“dp[i] 到底代表什么”这个问题,后面代码几乎是水到渠成的事。我在刷题笔记里反复强调一句话:“状态定义错了,后面全是白做。” 这句话是怎么来的?我拿最经典的“爬楼梯”举例,假设要爬到第 n 阶,每次可以走 1 步或者 2 步,问有多少种不同的方法。如果不假思索,很容易写出 dp[i] = dp[i-1] + dp[i-2] 这样的转移,但如果问你 dp[i] 是“走到第 i 阶的方案总数”,那么 dp[0] 该初始化为 1 还是 0 就会成为第一个坑。很多题解直接说 dp[0]=1,理由是“0 阶也算一种方案”,但这对新手来说并不自然。换个定义方式,dp[i] 表示“从第 0 阶走到第 i 阶的方案数”,然后让 dp[0]=1,dp[1]=1,你会发现递推关系依然成立,而且更好理解。这就是状态定义带来的连锁影响。
动态规划里第二重要的东西是遍历顺序。还是拿我刷的“最小路径和”来说,给定一个二维网格,从左上角走到右下角,每次只能向右或者向下走,要让路径上的数字总和最小。这是一道非常经典的二维 DP 题。状态定义很简单:dp[i][j] 表示走到网格 (i, j) 时的最小路径和,转移方程就是从左边或者上边较小的那一个再加上当前格子的值。但这里有一个新手很容易搞错的地方:初始化边界时,第一行只能从左往右累加,第一列只能从上往下累加,不能混着来。我曾经为了图快,直接在一个双重循环里处理所有格子,结果第一行和第一列的值计算错了,整个结果全盘崩掉。后来我的做法是:先把边界单独初始化,再处理内部格子,这样思维负担小,代码也更不容易出 bug。
2.3 回溯算法:把“撤销选择”想明白
第二天下下午我集中刷的是回溯。回溯本质上是深度优先搜索的一种特例,它的核心就是三个步骤:做选择、递归、撤销选择。很多人在学习时会被“递归”劝退,觉得太烧脑,但我用了一个生活化的类比来帮助理解:想象你要走出一个迷宫,你每到一个岔路口就做一个标记,选定一个方向往前走;如果走不通,你就按原路退回这个岔路口,把标记擦掉,再试另一条路。擦掉标记这个动作,就是代码里的“撤销选择”。
我刷“全排列”这道题时,第一次写出来的代码只加了递归,没有恢复状态,结果输出的排列里很多元素重复出现。因为当你递归完一条路回到上一层时,如果不撤销当前的“已使用”状态,下一层循环就会认为这个元素已经被占用了。这个撤销动作,代码上就三五行,但逻辑上非常关键。我自己的经验是,写回溯题的时候,拿笔在纸上画出递归树,把每一步的“选择”“递归”“撤销”分别标出来,真的比闷头写代码要快得多。虽然动笔看起来很慢,实际上很顶用。我这次刷题时几乎每道回溯题都会先画递归树,思路直接清晰了一个档次。
3. 刷题过程中的高频失误与排查实录
3.1 边界条件:一半以上的 bug 都出在这里
两天刷下来,我统计了一下自己犯的错,发现一个扎心的事实:真正因为“解法不会”而卡住的情况大概只占三成,剩下的七成全部是边界条件处理出了问题。比如数组越界、空输入、只有一个元素、整数溢出、字符串为空这些情况,不专门去测的话,代码很容易在“看起来没什么问题”的状态下挂掉。
我之前写“二分查找”就翻过车:默写下来很简单,但真正在 LeetCode 上跑的时候,死循环了。后来定位了半天,发现是 while 条件是 left < right 还是 left <= right,以及 mid 更新的时候要不要加一的问题。这两个问题不搞清楚,二分查找就是“玄学”。我整理出一个规律,供大家参考:
| 循环条件 | mid 更新方式 | 适用场景 |
|---|---|---|
| left <= right | right = mid - 1,left = mid + 1 | 标准区间查找,直接返回索引 |
| left < right | right = mid,left = mid + 1 | 寻找左边界 |
| left < right | right = mid - 1,left = mid | 寻找右边界,mid 需向上取整 |
这个表很重要,因为我发现很多边界问题不是能力问题,纯粹是“没有系统整理过”。比如你用的是左闭右闭区间,那 left <= right 是对的选择,否则空区间时循环直接退出,根本不会进入判断。但如果你写的是左闭右开区间,那循环条件又不一样了。写二分查找之前,先想清楚“我维护的这个区间到底长什么样”,再动笔,这样基本上就能避开很多坑。我把这段总结直接写进了笔记里,标注为“反复要看的重点”。
3.2 复杂度误判:一场隐藏的面试雷区
除了边界条件,还有一个容易被忽视的问题是对时间复杂度的误判。我在刷“最长回文子串”这道题时,最开始想到的是暴力枚举所有子串,再逐个判断是否是回文,逻辑倒是简单,但总复杂度是 O(n^3),数据范围稍微大一点就直接超时。后来我改成中心扩展法,枚举每个中心位置,然后向两边扩散,复杂度直接降到 O(n^2)。这道题让我意识到,刷题的时候不能只满足于“做出来”,还要逼自己想一想:“如果数据范围扩大十倍,我的代码还能跑吗?”
我自己的做法是,在每道题的笔记里都额外写一行“时间复杂度 & 空间复杂度”,并且在提交通过之后,刻意看一眼最优解的复杂度是多少,然后思考差距在哪里。像“最长回文子串”这种题,如果只看暴力解,你永远学不到 O(n) 的马拉车算法;但你如果已经掌握了 O(n^2) 的中心扩展法,再去看马拉车,理解起来就轻松不少。这个学习路径,比一上来就背马拉车要扎实得多。这也是为什么我特别建议刷题时不要一次性追求最优解,而是先有暴力解做保底,再一步步优化,这个“渐进优化”的过程本身就是宝贵的能力。
3.3 工具与环境的折腾:本地调试到底值不值得
刷题过程中还有一个很多人纠结的问题:到底是在网页编辑器里直接写,还是拉到本地 IDE 里配合测试用例调试?我个人这两天的体验是:本地调试非常值得,但要想清楚成本。我用的方法是,在本地建一个简单的测试脚本,把题目里的示例用例写进去,然后配合断点和 print 逐步观察变量变化,尤其是双指针、回溯这类需要观察“状态什么时候被修改了”的题目,这种方式能帮你秒级定位问题。
当然,本地调试也有坑。我之前在本地写得好好的代码,一粘到在线评测系统就报编译错误,后来发现是本地没有严格按照题目的输入输出格式来处理,比如没有处理多组输入、没有 trim 掉多余的空格等。所以我现在养成一个习惯:代码通过本地测试以后,还会专门检查一下输入输出的格式,再提交到评测系统里。这个习惯让我避免了很多“毫无意义的失败提交”。
4. 刷题效率工具体验与笔记方法分享
4.1 我的错题本:分层记录法
这次刷题,我做了一个很有效的改变:分层记录错题,而不是把所有东西混在一个文档里。我分成了三层:第一层是“思路错误”,这类错题意味着我完全没方向,需要在笔记里重新写清楚这类题型的套路与思考路径;第二层是“边界条件错误”,这类题我解法大方向是对的,但在特殊用例上出了问题,因此我会在笔记里用红字专门标注边界值,比如空数组、负数、最大整数等;第三层是“代码实现错误”,比如变量名写错、索引不对、忘了修改循环变量,这种我会随手写一句“低级失误,下次注意”,而不需要花太多时间。
这个分层记录法帮我节省了很多复习时间。因为第一层错题才是真正值得反复看的,第二层只需要考前扫一眼,第三层干脆可以忽略。刷题到最后,最忌讳的其实不是错得太多,而是错得太多但从不分类,结果复习的时候被海量信息淹没,什么都想抓,什么都抓不住。
4.2 笔记里的“一句话总结”有多重要
我在每一道题的笔记末尾,都会逼自己写一句“一句话总结”。比如“三数之和:排序 + 双指针,注意跳过重复元素”;“爬楼梯:斐波那契,状态压缩,空间优化到 O(1)”。别小看这一句话,它其实是在强迫我用最精炼的话把这道题的核心讲清楚。如果一句话说不清,说明我对这道题的理解还不够深,那就得再回去看看。
而且这种一句话总结,等到后面复习的时候特别有用。两天结束之后,我翻看自己写下的这二十多句话,基本就能在十几分钟内把所有题目的套路在脑子里过一遍,相当于做了一个快速索引。如果当时没写这句话,那我复习时重新看每道题就得从完整笔记里重新读一遍,效率会低很多。
5. 实操模板与通用解法清单
5.1 经典题型的“最小可用代码”模板
刷完这两天,我最大的收获之一,是整理出了一批“最小可用代码”模板。所谓最小可用,就是删掉所有花哨的写法,只保留最核心的逻辑结构,方便在面试紧张的时候快速回忆起来。这里我挑几个分享出来:
二分查找模版(左闭右闭):
def binary_search(nums, target): left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return -1回溯模版:
def backtrack(path, choices, result): if 满足结束条件: result.append(path[:]) return for choice in choices: 做选择 backtrack(path, choices, result) 撤销选择滑动窗口模版:
left = 0 for right in range(len(s)): 更新窗口状态 while 窗口不满足条件: 收缩左边界 left += 1 更新结果这几个模板我写在了笔记最前面,作为一个快速参考区。面试前我通常会把这些模板快速过一遍,让自己进入状态。要注意的是,模板是帮你在紧张时有东西可以写,但不代表你不需要理解背后的原理。有几次我只是机械地套模板,结果题目稍微变形就写了很久,后来才意识到模板必须配着原理解释一起记忆,才真正有用。
5.2 两天 20 道题的完整清单与复盘
这里把我实际完成的部分题目列表列出来,给粉丝们一个参照。当然题目数量不是核心,关键是覆盖面:
| 分类 | 题目 | 核心考点 | 我的错误/收获 |
|---|---|---|---|
| 哈希表 | 两数之和 | 空间换时间,字典存储 | 想到了哈希表,但忘记处理重复元素 |
| 双指针 | 三数之和 | 排序 + 双指针 + 去重 | 去重逻辑不熟练,第一次漏掉 |
| 滑动窗口 | 无重复字符的最长子串 | 窗口左边界更新 | 忘记取 max,左指针回退 |
| 二分查找 | 搜索旋转排序数组 | 二分变体 | 区间判断条件写反,调试很久 |
| 动态规划 | 爬楼梯 | 斐波那契,状态压缩 | 老题,但要考虑 dp[0] 的定义 |
| 动态规划 | 最小路径和 | 二维 DP,边界初始化 | 第一行第一列处理错误 |
| 回溯 | 全排列 | 撤销选择 | 漏掉撤销,结果重复 |
| 回溯 | 子集 | 选或不选两种视角 | 迭代法思路更清晰 |
| 二叉树 | 二叉树的中序遍历 | 递归与迭代(栈) | 迭代写法忘记维护 left 链 |
| 二叉树 | 二叉树的最大深度 | 后序遍历 | 递归思路清晰,一行代码解决 |
复盘完这张表,我最大的感受就是:我犯的错真的很典型,而且大多集中在“对状态变化的理解不够深”上。比如滑动窗口左指针回退、回溯漏撤销、DP 边界初始化错误,归根结底都是没有想清楚“每一步操作之后,状态应该是什么”。所以后面的刷题复习里,我也调整了策略:每道题写完之后,多花三十秒在脑子里把“状态变化”过一遍,而不是急着去做下一道题。这种方法,比单纯追求“今天刷了十道题”的成就感要有效得多。
6. 两天刷题后的直接经验与建议
6.1 给不同基础的人两条刷题路线建议
刷题这件事,其实很讲究“匹配度”。我之前也走过弯路:刚开始的时候,一上来就刷 hard 题,结果被虐得体无完肤,自信心严重受挫。按照这两天的体验,我给不同基础的朋友两条路线参考。如果你基础比较薄弱,连树的遍历都还有点陌生,我建议先从“数据结构基础题”入手,比如数组删除元素、链表反转、用栈实现队列,这一阶段目标不是会做难题,而是熟悉语言 API、掌握常见数据结构的基本操作,至少刷三十道左右再进入下一阶段。如果你已经能独立做出简单题,只是中难题容易卡住,那我的建议是主攻“高频算法专题”,就是我今天写的双指针、滑动窗口、动态规划、回溯这几个方向,每个专题至少连续刷十道,再交叉混合训练,强迫自己识别题型。
6.2 时间管理与心态调整的碎碎念
两天高强度的刷题,说实话挺累的。尤其是第二天下午写回溯的时候,我的脑子明显感觉转不动了,看递归调用栈看了半天都没绕出来。后来我站起来休息了十五分钟,洗了把脸,再回来重新画递归树,才成功把思路理清。这个过程让我意识到:刷题本质上也是一个体力活,集中刷题时一定要给自己留出足够的休息时间,不要硬撑。我还发现一个小技巧:把手机放到另一个房间,只留一台电脑和一本书在书桌上,减少外界干扰,这样进入心流状态的速度会明显变快。这种专注状态一旦建立,刷题效率能翻倍,而且对思路连续性的帮助特别大。
那两天里我还有一个心态上的变化:接受“不会做”是常态,而不是耻辱。很多人在刷题时遇到不会的题,第一反应是烦躁,然后马上去看题解。我的做法是:先自己硬想十五分钟,如果十五分钟后还是毫无头绪,再去看题解。但看完题解之后,不是直接复制粘贴,而是把题解关上,用自己的话从头写一遍代码,哪怕写出来的和题解不完全一样也没关系。这个过程很残忍,但见效极快。刷完两天题,我能明显感觉到,自己面对陌生题目时的第一反应已经从“好难”变成了“先分析一下题型和可能的解法”,这种思维方式的转变,可能比多会做几道题本身更重要。
我个人在实际操作中的体会是,刷题笔记这类东西,最重要的不是记了多少,而是回头看多少。很多人的笔记写完就吃灰,那就失去了意义。我这两天的笔记虽然只是简单的 Markdown 文档,但我在里面加了大量的复盘和标注,目的就是让一周后的自己还能快速看懂当时卡壳的地方。如果你也想尝试集中刷题,我真心建议你也建一个自己的错题分层体系,哪怕只是拿个记事本,也比什么都记不住要强得多。希望这份刷题笔记能给你一点启发,让你找到适合自己的节奏。