刷题53天复盘:从LeetCode经典题到算法思维系统化
1. 第53天当刷题日历进入“疲劳段”day53 括号里写着 1.4我看了一眼意识到这是今天的日期。1月4日不是什么特别的日子但如果把它理解成“第1轮的第4个阶段”倒也挺合适——我正式开始按体系刷题已经53天主线是 LeetCode 面试经典150中途穿插周赛和旧题回顾。坦白讲第20天左右我的状态最好每天开新题都兴奋觉得算法水平肉眼可见地在涨。到了第40天新鲜感明显退了。每天打开题库不再是“今天要学新东西”的期待而是“今天必须完成配额”的自律。这种情绪变化不是坏事只是说明我得调整玩法。所以day53这篇文章我不想做成题解集锦更想认真复盘一下这段时间里刷题方法本身出了哪些问题以及我在哪些题上真正吃亏过、又是怎么爬出来的。先说说我为什么选“面试经典150”。市面上的热门题单很多最常见的是“LeetCode 热门100题”。那个榜单偏流量思维收录的确实是高热度题目但题与题之间缺少明显的知识点梯度。热门100更像一个“散装全明星阵容”适合已经有基础的人查漏补缺对于像我这种想重新搭一遍算法框架的人它让我总觉得哪里哪里的基本功还欠着火候。面试经典150虽然热度不如热门100那么夸张但它的编排有意图数组、字符串、双指针、滑动窗口、矩阵、哈希表、区间、栈、链表、树、图、回溯、二分查找、栈与优先队列、动态规划一层层铺开。我是在刷完约60题之后才真正感受到这个编排的价值——每个专题下的小题都在给后面的大题铺路。比如二分查找专题里先出现“在排序数组中查找元素的第一个和最后一个位置”后面再遇到“爱吃香蕉的狒狒”时我已经会自动往“二分答案”上靠了。53天里我最明显的感受是刷题这件事前30天靠热血后面全靠反馈机制。没有反馈打卡就成了自我感动。所以下面我挑了三道这段时间里真正卡过我、也让我想明白一些事情的代表题来做完整复盘。2. 三道卡住我的题最长回文子串、目标和、爱吃香蕉的狒狒2.1 最长回文子串为什么我先放弃Manacher“最长回文子串”在面试经典150里属于动态规划专题也是LeetCode第5题。题目很短给定一个字符串 s返回其中最长回文子串。我第一次拿到就想到暴力枚举每个子串再判断是否为回文复杂度是 O(n^3)结果自然是平庸的。接着想到动态规划用 dp[i][j] 表示 s[i..j] 是否为回文如果 s[i] 等于 s[j]并且内部也是回文那么 dp[i][j] 就成立。核心转移方程是dp[i][j] (s[i] s[j]) and (j - i 2 or dp[i 1][j - 1])这个方程的关键在于当 j - i 2 时意味着子串长度小于等于3只需要判断两端字符是否相等因为中间部分要么是空要么是单字符天然是回文。我在纸上手动模拟了“babad”和“cbbd”两组用例确认边界没问题后才动手写代码。循环时需要保证从短子串推导长子串所以外层遍历右边界 j内层遍历左边界 i否则 dp[i1][j-1] 可能还没计算出来。AC之后我看了题解发现不少人推荐 Manacher 算法复杂度能优化到 O(n)。这个算法我理解过一遍也承认它确实优雅但我在面试场景里不会首选它。原因有两点第一Manacher 需要维护中心、右边界和对称半径数组思路和细节都比较绕临场写很容易出低级错误第二大多数面试官在问到这道题时期待的是你能讲清楚动态规划的状态设计和边界条件中心扩展写法其实是更通用且不容易出错的方案。中心扩展的思路非常朴素回文串是以某个位置为中心左右对称的那我枚举每一个可能的中心位置然后向左向右同时扩展直到两边字符不相等为止。中心有两种情况奇数长度回文的中心是一个字符偶数长度回文的中心是两字符之间的缝隙所以每个位置要分别计算一次奇数中心和一次偶数中心。核心代码只有十几行def expand(s, left, right): while left 0 and right len(s) and s[left] s[right]: left - 1 right 1 return right - left - 1 for i in range(len(s)): odd expand(s, i, i) even expand(s, i, i 1) cur max(odd, even) if cur end - start: start i - (cur - 1) // 2 end i cur // 2这里返回的 right - left - 1 是回文长度因为最后一次 left 和 right 已经越界或字符不等需要把两边缩回来。每轮扩展的时间摊到整个字符串上是 O(n)总共 O(n^2) 的复杂度。面试如果继续追问我再补充说还有一个 O(n) 的 Manacher 方案但实现较复杂这样反而显得对复杂度有全局认识。这道题给我的第53天式提醒是题解库里炫技的解法很多但面试是限时场景写一个半小时内不会出错的方案比写一个看起来很高级但容易忘记边界的方案重要太多。我后来刷专项时也发现动态规划专题里很多题都存在“思路上限高、代码量不大”的特点真正拉开差距的是边界处理能力。2.2 目标和你以为在DFS其实它是背包问题“目标和”在LeetCode里的题号是494属于面试经典150里“一维动态规划”的进阶题。题干很直白给你一个非负整数数组 nums 和一个整数 target你可以给每个数组元素前面添加正号或负号然后串联起来求表达式返回表达式结果等于 target 的不同方案数。我最初很自然想到DFS枚举每个数的正负号毕竟每个位置只有两种选择回溯模板能直接套。但稍微算一下就知道数组长度最大能到20左右时2的20次方约一百万还能勉强接受如果长度更长指数级扩展就直接爆炸。于是我开始找规律。手动算几个例子后会发现一个很有价值的转化。设所有添加正号的数字之和为 P所有添加负号的数字之和为 N那么有P N sum(nums) P - N target两式相加得到2P sum(nums) target所以 P (sum(nums) target) / 2。这意味着问题被转化成了从 nums 中选出一个子集让子集和等于 P问有多少种选法。这不就是典型的01背包计数问题吗每个数字只能选一次目标是凑出容量 P。需要注意这个转化的前提是 sum(nums) target 必须是非负偶数。如果它是负数或者奇数直接返回0根本不需要做后续动态规划。这个前置检查特别容易漏我第一次写就是因为没处理 target 绝对值大于总和的情况有一组测试用例怎么都过不去。动态规划部分很标准用 dp[j] 表示能凑出子集和 j 的方案数total sum(nums) if (total target) 0 or (total target) % 2 ! 0: return 0 cap (total target) // 2 dp [0] * (cap 1) dp[0] 1 for x in nums: for j in range(cap, x - 1, -1): dp[j] dp[j - x] return dp[cap]内层循环为什么必须从右往左因为01背包要求每个物品只能用一次。如果从左往右遍历那么 dp[j - x] 可能已经在当前物品的迭代中被更新过就会出现同一物品被重复使用的情况。从右往左遍历dp[j - x] 是上一轮的状态正好保证每个数字被考虑后只会影响一次结果。还有一个不少demo没细说的细节如果 nums 里包含0上面的倒序循环应该怎么写。最稳的办法是先统计零的个数 zero_count然后只对非零部分做01背包最终答案乘以 2 的 zero_count 次方。因为每个0对子集和不产生变化但可以选择加正号或负号每个0都会让方法数翻倍。很多写法里对0的处理是让容量循环到0那样也能得到正确答案但第一次接触的人容易晕。先去掉0、最后乘倍数思路更清晰。复盘这道题我最大的收获是有些题长得像回溯本质却是背包。如果拿到题只按表面结构选模板很容易写出一个“能跑但很慢”的DFS。反过来如果你对背包模型足够敏感看到“正负号组合”这类条件会主动去想P和N的代数关系。这就是刷专题的意义——把数据类型和解法模型的对应关系刻进肌肉记忆。2.3 爱吃香蕉的狒狒二分答案吃透“最小可行速度”题目里的“吃香蕉”其实对应LeetCode第875题。场景很形象狒狒面前有若干堆香蕉第 i 堆有 piles[i] 根香蕉守卫离开 h 小时狒狒每小时会选一堆香蕉吃掉 k 根。如果某堆不足 k 根它也会把这堆吃完但这一小时内它不会再去吃别的堆。题目要求的是在保证 h 小时内能吃完所有香蕉的前提下最小的 k 是多少。我承认我第一次做的时候想到的是模拟从 k1 开始逐步增加检查每个速度是否能完成任务但 piles 数组长度和数值都可能很大线性枚举不合适。卡住的原因是我没意识到这是一个典型的“二分答案”问题。所谓的二分答案不是对数组下标二分而是对“问题解的可能区间”做二分。这里的解 k 有明确的物理范围最小是1因为速度不可能小于每小时1根最大是 max(piles)因为如果速度超过最大堆的数量每小时也只能吃一堆再大不会节省时间。在这个范围里我们用某个速度 k 去计算总耗时耗时会随着 k 增大而单调减少。既然具有单调性就可以二分找最小可行值。check函数的核心在于每堆耗时计算不能直接写成 pile // k因为当 pile 不能被 k 整除时狒狒吃完这一堆需要额外一小时。正确的向上取整写法是def can(k): hours sum((pile k - 1) // k for pile in piles) return hours h然后套用“找左边界”的二分模板lo, hi 1, max(piles) while lo hi: mid (lo hi) // 2 if can(mid): hi mid else: lo mid 1 return lo这里有几个容易踩的点。第一个是向上取整不要写 int(pile / k) (pile % k ! 0)虽然逻辑一样但用 (pile k - 1) // k 更简洁也不会因为浮点数精度出问题。第二个是 hi 的初始值设成 max(piles) 就够不用设成 sum(piles)因为当 k 等于最大堆的数量时每次吃一堆最多一小时已经是最优速度更大的 k 只会浪费。第三个是“每小时最多吃一堆”这个约束它意味着总耗时等于每堆耗时之和而不是总香蕉数除以速度。有人会把问题简化成“总根数除以k”那就完全做错了。这道题给我的启发很直接当答案具有单调性并且你能快速验证某个候选值是否可行时优先考虑二分。我后来看到“求最小最大”“求最大最小”之类的问题条件时都会下意识画一个“横轴是答案、纵轴是是否可行”的草稿如果函数是分段常量且单调的那基本就是二分答案的菜。“爱吃香蕉的狒狒”其实根本不吃香蕉它吃的是套路。3. 第53天我的刷题系统做了哪些调整3.1 三色标记法不再只靠AC判断自己会了在刷题前期我习惯性的判断标准是“提交通过”。后来发现这个标准害我不浅。一道题看题解写完并AC很可能只是复制了别人的思路过三天再让你手写照样写不出来。所以我从大概第30天开始用三色标记法管理做题记录。红色代表完全没思路看了题解才勉强写出来。黄色代表有大致方向但实现时卡在某个边界或细节上需要瞟一眼提示。绿色代表能独立写出并通过还能讲清复杂度和为什么这么做。每天晚上我会统计当天题目的颜色如果一个周期内红色占比过高说明节奏太快需要回落如果连续几天都是绿色就该适当提升难度否则可能是在舒适区里打转。这个标记法不只是一个管理工具它其实在逼我给每道题写“一句话注释”。红题我会写为什么没想到这个思路突破口是什么黄题我会写卡在哪一个边界绿题我会写如果面试官追问优化我该往哪个方向说。这些注释成了我复习时的第一手材料比收藏一堆题解文章有用得多。3.2 新题与旧题的配比每天32第40天的时候我意识到一个问题每天猛刷5道新题看似效率高但月底回头一看上上周做过的题已经忘了一半。后来读了一些关于记忆周期的资料也结合自身感受我把每日任务改成“3道新题 2道旧题复现”的结构。旧题不是简单看一遍题解而是调出之前的空编辑器完全不看任何记录把题目重新做到AC。如果超过25分钟没做出来我才会去翻自己的注释。这个“25分钟红线”也不是随便定的一道题如果曾经被我标成绿题25分钟还写不出来说明当时的绿可能是虚假繁荣我会把它降级成红色并安排在下周的复习列表里。复现旧题时我强烈建议清空编辑器里面的历史代码新建一个文件重新写。因为人会骗自己。看自己曾经的代码会觉得“嗯这个逻辑我懂”可一旦关掉参考手立刻会诚实。53天积累下来我给自己最值钱的资产不是AC数量而是那两百多道旧题里真正经得住时间检验的几十道核心题。3.3 新题思考时间控制在35分钟以内有一段时间我特别迷恋“死磕”一道难题可以耗一个半小时不看题解。这种精神固然可敬但效率太低。尤其面试准备期时间紧、任务重每题都死磕等于把时间花在了单点上。我现在给自己定的是35分钟思考红线。计时开始后前10分钟只做一件事把题目的条件翻译成自己的话并且把所有可能的暴力解法先写出来。这一步非常重要因为它能帮我确定问题的规模上限判断直接搜索会不会超时。中间15分钟尝试给问题做转化想想这种类型的条件以前在哪个专题里见过。最后10分钟如果还是毫无头绪我就大大方方打开题解。有人会觉得35分钟是不是太短我的经验是如果一道题你完全没有思路即使再给3小时也是同样的思路广度因为你缺少的是背后的模型不是思考耐心。真正需要死磕的题极少多数题目卡住你的点在于没见过那个经典转化而看题解之后记录“我为什么没想到”就是最划算的学习方式。等二刷、三刷时能独立写出来这题才真正归你。4. 周赛430我不再追求AK之后的复盘逻辑第53天的时间点刚好挨着一场周赛我打的是第430场周赛。这次周赛成绩不算亮眼但对我的心态调整帮助比任何题目本身都大。4.1 赛前设好退出线拿到两题保底就算达标以前打周赛我总有一种病态心理希望每场都全过四题一旦卡住就会狂躁越急越写不出来。打到第40分钟左右如果前两题还没过后面基本就是崩盘状态。这次我学聪明了比赛开始前先给自己划了三道线。第一道线是保底线T1和T2必须稳住这两题通常对应基础数据结构或简单枚举是最不该丢的分。第二道线是进取线T3如果30分钟内有一点思路就试着冲一下思路混乱就果断放弃。第三道线是幻想线T4直接不给自己压力能做出来当赚了做不出来绝不emo。这种预设退出线的核心在于周赛本质上是一次每周自测它的价值是暴露问题不是制造焦虑。我在第430场周赛里最后只稳住了两题半第三题卡了20分钟后果断放弃并去复盘结果发现它考察的知识点是我刷面试经典150时见过的一类但我在题解里从来没有主动整理过对应的通用模板所以赛场上一紧张就想不到。这个发现比我硬磕完三题更有价值它直接告诉我下一周专题要补哪里。4.2 赛后复盘只盯两个问题周赛结束后我不会把四道题全部完整重写一遍那样投入产出比不高。我只回答自己两个问题第一我在赛场上浪费时间的操作是什么第二卡住我的那题对应哪个专题盲区第一个问题的常见答案有很多种。比如我发现自己有个坏习惯写T2的时候明明想到了两个候选方案却舍不得立刻固定一个总想在脑子里反复比较哪个更优结果多花了10分钟。后来我给自己立了规矩周赛里只要有一个能通过小数据验证的思路就先把它写出来AC后再去想优化而不是一开始就在脑子里做完美主义设计。这个改动让我的代码产出速度快了不少。第二个问题则直接指向下周的准备计划。第430场周赛复盘后我把“如何识别状态压缩DP的适用场景”这个知识点列进了下一周的学习清单。它不是面试经典150里的高频内容却是我这个阶段的明显短板。如果不打周赛我大概还觉得自己掌握得不错。4.3 周赛和面试经典150的关系是互补很多人把周赛当成“高手炫技场”觉得自己还在刷基础题就不配参加。我一开始也这么想直到某次周赛里我被迫用很笨的方法过了一道题回头发现那道题的核心解法竟然来自经典150里的专题知识点才知道周赛并没有想象中那么遥远。周赛的题目对速度和临场反应要求更高但思维模型就是那些基础专题的组合。面试经典150更像训练场周赛更像试衣间。训练场里你可以反复试错试衣间里你需要在有限时间里展示最佳状态。每天刷经典题是在扩宽知识的边界每周打一次周赛是在检验平时训练有没有真正内化。两者不是非此即彼的关系而是循环周赛暴露短板然后在150题里找对应专题补齐再去下一场周赛重新验证。如果你想验证自己某段时间有没有进步不是看AC数而是看最近几场周赛的“放弃时间点”有没有推迟以及复盘时能不能一眼说出某题的考点名称。这个标准比名次和分数更能反映底层能力的提升。5. day53之后我给自己定的三条新规则5.1 规则一题做出来之后还要对着空气讲一遍第53天前我已经听很多人说过费曼学习法但一直没认真执行直到我开始把某些经典题的解法讲给一个完全不懂算法的朋友听才意识到自己有多少地方是“好像懂”。讲出来的过程会强迫你把概念变成有逻辑顺序的语言比如动态规划里为什么要倒序遍历容量、二分答案里为什么 mid 的更新分支长这样如果这些地方只能靠“我记得是这样写”来糊弄其实说明你还没有真正理解它。我现在每道绿色标记的题会要求自己不看代码在空白处写下三句话这题属于什么类型最优解的核心状态是什么边界条件最容易在哪里出错。能写出来才算真正AC写不出来就退回黄色重做。这个方法让我每天的新题数量从5道降到3道但长期看效率反而更高。5.2 规则二周末选一个专题做纵向对比每天按顺序刷题容易陷入“只见树木不见森林”的境地。比如动态规划在数组、字符串、背包、区间不同场景下的状态定义差别很大如果一天只做一道题很难总结出规律。从第54天开始我计划每个周末集中刷同一个专题下的3到5道题做完后不急着写下一题而是拿出一个空白文档把这几道题的状态设计、转移方程写在一起做对比。说白了题目刷多了以后真正能迁移的不是某个代码模板而是状态设计时的思考习惯是问“以i结尾的子序列”还是“前i个元素中选j个”是问“到第i个位置的最小代价”还是“在i位置时的最大利润”这些分类一旦刻在脑子里很多陌生题都能快速套到熟悉的框架里。系列化的纵向对比比每天做一道新题的涨知识效率高太多。5.3 规则三允许自己“刷不完150题”最后这条可能听起来有点反直觉但它是第53天我最想对自己说的话。刷题计划卡到中段时焦虑感常常来自“还剩多少题没刷”总想把150道全部啃完才有信心去面试。但事实上面试不会从题单里按编号抽题它考的是你解决问题的方式而这种方式在刷透某一半题目之后就已经逐渐成型了。与其纠结第120题还有没有做我更想把已经做过的那些核心题的每种解法吃透保证任何一道绿色标记的题在三个月后依然能独立做出来。对我来说这比“刷完全部150题”的成就感更有实用价值。如果你也在刷这套题单坚持到了差不多一半的进度我想让你知道慢一点不可怕怕的是只顾往前赶把每道题都做成了“一次性用品”。第53天我的日历翻到了1.4算法能力没有质变也没遇到所谓“顿悟时刻”但回头看这些留下的注释、颜色和错题记录我能感受到自己的思考方式在悄悄变得系统。刷题这件事最终会结束但“拿到陌生问题后该怎么拆解、如何选择模型、怎么快速验证边界”这套流程大概是这段时间留给我的比任何一道AC题都值钱的东西。