资讯动态

力扣每日一题一周复盘:BFS、栈、二分、双指针、单调队列全覆盖

发布时间:2026/10/13 4:14:24 来源:尧图企业网站定制
从 1 月 19 日到 1 月 25 日这一周的 LeetCode 每日一题我完整跟了一遍。项目标题听起来只是“日历打卡”实际上这一周过得相当充实既有 994 腐烂的橘子这样的 BFS 经典又有 224 基本计算器这种让人头皮发麻的表达式求值中途还穿插了狒狒爱吃香蕉、接雨水、滑动窗口最大值这些高频常客周六又撞上周赛 430。如果你正处于准备面试的阶段想找一个“既上强度又覆盖核心题型”的七天样例这一周的题目安排就很值得拆开研究。这篇文章我会把每天的题都拉出来过一遍讲清楚每题背后的算法原型、代码怎么落地、边界在哪里再补上我实际操作中踩过的坑。最后一part也放了连续刷题的方法论适合刚入坑 LeetCode 的人收藏着看。我保证不只是贴代码而是把“为什么这么写”说透。1. 本周每日一题盘点七天七种套路1.1 这一周的具体安排先把题目列出来方便你对号入座日期题目核心标签一句话点评1月19日周一腐烂的橘子994多源 BFS图遍历的入门试金石层数计数写错会全盘崩1月20日周二基本计算器224栈、括号展开表达式类题目的鼻祖细节极其多1月21日周三狒狒爱吃香蕉875 / 剑指 Offer II 073二分答案最标准的二分查找模板题考“最小速度”1月22日周四接雨水42双指针、前后缀最大值热门 100 题里的常青树思路多样1月23日周五滑动窗口最大值239单调队列队列维护的是索引不是值理解了就通1月24日周六周赛 430综合四道题一套组合拳适合检验周中积累1月25日周日复盘 / 补题综合我用来做错题沉淀和重写题解周一到周五基本是按照“算法思想”铺开的图论 → 栈 → 二分 → 双指针/前缀最值 → 单调队列。每一种思想之间没有重复意味着你每天都要换一套思维模式。这种“换脑子”的安排其实比连续刷十道同类型的题更有价值因为真实面试和竞赛不会提前告诉你这题要用哪个算法。1.2 为什么这个组合值得写一篇复盘说实话之前连续几周的每日一题经常是“变着法子出新题”难度忽高忽低每周都暴露出不少知识盲区。这次的好处在于每一道都是原汁原味的经典题覆盖面正好是算法面试里出现频率最高的几个方向。我可以负责任地说如果你能把这一周刷透LeetCode 热门 100 题里至少有三四十道题你已经摸到门了。还有个值得注意的点是“同类型题目的纵向对比”人家的每日一题可能选两到三个相似的题放在相邻几天里帮你巩固但这次硬生生把五种算法的题放在同一周里。你写完腐烂的橘子第二天就要切换栈和括号思维这种突如其来的“跳频道”恰好模拟了周赛和真实面试的状态。我在周赛 430 里就明显感觉到前五天练的“快速识别题型”能力是能迁移的拿到题先判断是 BFS、二分还是单调栈比闷头写代码重要得多。2. 腐烂的橘子994多源 BFS 与层级计时的底层逻辑2.1 一眼识别“多源 BFS”题目什么意思我不再重复简单概括就是一个二维网格里有新鲜橘子、烂橘子和空格每一轮烂橘子会把上下左右相邻的新鲜橘子感染变烂问多少轮之后全部腐烂。这是个很典型的“状态的扩散”问题而且扩散速度恒定为一轮一个格子。遇到这种“从多个起点同步向外扩散”的问题第一反应必须是 BFS 而不是 DFS原因很直接BFS 天然按层扩展每一层对应一轮感染层数就是要求的分钟数DFS 需要额外维护路径和访问顺序很难表达“同时感染”的并发语义多个初始烂橘子起点互不干扰用队列把它们全部入队就是多源 BFS。这里有个生活化的类比一堆火源同时着火了火势每秒向四周蔓延一格你要算多久烧完一片森林。如果用一个 DFS 去模拟“先沿着一条路走到黑”你会得到完全错误的蔓延时间因为现实中所有火源是同步在烧的。队列天然尊重这种同步性。2.2 可复用的 Python 模板先看代码这套模板可以直接背from collections import deque from typing import List class Solution: def orangesRotting(self, grid: List[List[int]]) - int: m, n len(grid), len(grid[0]) q deque() fresh 0 # 第一步把所有初始烂橘子入队同时统计新鲜橘子数量 for i in range(m): for j in range(n): if grid[i][j] 2: q.append((i, j)) elif grid[i][j] 1: fresh 1 # 特判如果根本没有新鲜橘子不需要任何分钟 if fresh 0: return 0 minutes 0 dirs [(1, 0), (-1, 0), (0, 1), (0, -1)] # 第二步逐层 BFS一层代表一分钟 while q: minutes 1 for _ in range(len(q)): x, y q.popleft() for dx, dy in dirs: nx, ny x dx, y dy if 0 nx m and 0 ny n and grid[nx][ny] 1: grid[nx][ny] 2 fresh - 1 q.append((nx, ny)) # 如果某一轮结束之后新鲜橘子已经清零直接返回 if fresh 0: return minutes # BFS 结束还有剩余新鲜橘子说明它们被隔离了永远无法被感染 return -1注意一个细节minutes是在进入当前层时先加一。因为我用的是“先加时间再处理本层”的逻辑所以不会出现多算一分钟的情况。如果你习惯用 distance 数组记录每个格子的腐烂时刻那也是常见解法核心思想和这个一样只是代码略长。时间复杂度 O(mn)因为每个格子最多入队一次空间复杂度 O(mn)主要是队列和最坏情况下的格子数量。2.3 我踩过的两个边界坑第一个坑是“无新鲜橘子”的特判。很多人一上来就把所有烂橘子入队开始 BFS最后发现输入是[[0,2]]时返回了-1。正确语义是一开始就没有新鲜橘子需要 0 分钟。所以上面的代码在 BFS 之前先做了if fresh 0: return 0这个特判不能省。第二个坑是“新鲜橘子计数放在 BFS 外边还是循环里”。我见过有人每次都重新扫描全网格找新鲜橘子变成了 O(mn) 次扫描复杂度相乘最后提交超时。正确做法是维护一个全局fresh计数感染一个就减一BFS 结束时看它是否大于 0。这是一个非常典型的“用变量代替重复遍历”的优化思路在后期很多复杂题里都会复用。3. 基本计算器224栈、符号与括号展开3.1 只有“加号和减号”反而更考细节和 227 基本计算器 II 相比224 只包含加减和括号没有乘除表面上难度降了一截。但真正做过的人都知道括号带来的符号翻转才是最大的坑。例如1-( -2)这种表达式一眼看去似乎是两个减号叠在一起但如果你用“当前符号 sign”的思维去处理它其实并不复杂。这里的核心技巧是不要试图“分析括号内部”而是把括号看作一个“重置现场”的信号。当遇到(时把当前已经算好的结果和当前符号压入栈中然后重置结果和符号开始计算括号内的表达式当遇到)时从栈里弹出之前的符号用它乘以括号内的计算结果再加上之前已经算好的结果。这个过程说白了就是“先算局部再把局部合并进全局”。我自己的理解是栈里存的不是数字而是“上下文”。这两条上下文按顺序分别是进入括号之前已经累计的结果和进入括号之前的正负号。只要你明白这一点写代码时就不会把压栈顺序记混。3.2 关键代码与手动推演下面是很经典的写法class Solution: def calculate(self, s: str) - int: n len(s) sign 1 # 当前符号 res 0 # 当前累计结果 stack [] # 用来保存括号前的上下文 i 0 while i n: ch s[i] if ch.isdigit(): num 0 while i n and s[i].isdigit(): num num * 10 int(s[i]) i 1 res sign * num i - 1 elif ch : sign 1 elif ch -: sign -1 elif ch (: stack.append(res) stack.append(sign) res 0 sign 1 elif ch ): res stack.pop() * res stack.pop() i 1 return res拿1-( -2 )手推一遍遇到1res 1遇到-sign -1遇到(压入1和-1重置res 0、sign 1遇到-sign -1遇到2res -2遇到)执行res sign栈顶 * res res栈顶 (-1) * (-2) 1 3。因此最终答案是 3。很多人想不通为什么1-( -2 )等于 3其实你不需要在字符串层面处理“两个减号变成加号”只需要让括号内的2被外层符号再翻转一次数学上自然就对了。3.3 从 224 到 227、772 的拓展刷完 224 之后建议立刻做 227 基本计算器 II。那个题加入了乘除而且没有括号考察的是运算符优先级。核心思路从“一个符号变量”升级成“两个栈”一个数字栈一个运算符栈遇到乘除就立刻计算遇到加减则先压入栈等表达式扫描完再从栈顶依次弹出。很多人在 227 里栽在减法顺序上比如2-3-4如果从右往左弹栈会得到错误结果2-(3-4)。解决办法是让最后弹栈时用左操作数减去右操作数或者干脆在入栈时用负数表示减法。再往上是 772 基本计算器 III那就是既有括号又有乘除还可能有变量难度直逼竞赛题。我在实际刷题时给 224 的定位是“必须 15 分钟内写出来的范围”因为它在面试真题池里出现频率太高了。如果连 224 的压栈/弹栈顺序都要现场推导那面试压力会非常大。我的建议是至少把 224 和 227 背到肌肉记忆再拿到 772 去做拓展开脑洞。4. 狒狒爱吃香蕉875 / 剑指 Offer II 073二分答案的标准模板4.1 把“求速度”变成“验证可行性”狒狒爱吃香蕉是另一道“看起来不像二分实际上就是二分”的题。它的描述比较绕狒狒一小时只能吃一堆香蕉若一堆吃完就等着下一小时求能在 h 小时内吃完所有香蕉的最小速度 k。你拿到题首先要想清楚如果速度 k 很小需要的小时数就多如果 k 很大需要的小时数就少。也就是说随着 k 增大“能否在 h 小时内吃完”这个结果是单调的从不可能变为可能而且一旦可能更大的 k 一定也可行。这种“单调性是二分法能成立的根本前提”。那么问题就变成了在速度区间里找到一个最小的可行值。这和我们平时做“在有序数组中查找第一个满足条件的元素”完全一样只不过 check 函数不再是简单的比较大小而是计算“用速度 k 吃完所有香蕉需要多少小时”。我经常用一个类比你要在一个范围内猜一个最低出价能让供应商接受猜高了虽然能成交但多花钱猜低了供应商不发货。所以每次取中间价去验证高了就往下压低了就往上涨。这就是二分答案的思想LeetCode 里大量“最大化最小值”“最小化最大值”都是这个套路。4.2 二分区间与 check 函数的精确实现来看代码from typing import List class Solution: def minEatingSpeed(self, piles: List[int], h: int) - int: # check 函数速度 k 时吃完所有香蕉需要的小时数 def feasible(k: int) - bool: hours 0 for p in piles: hours (p k - 1) // k return hours h left, right 1, max(piles) while left right: mid (left right) // 2 if feasible(mid): right mid else: left mid 1 return left最妙的部分其实是(p k - 1) // k这是一个“向上取整除法”的经典写法。比math.ceil(p / k)更快也避免了浮点数精度问题。比如一堆香蕉有 10 根速度 k3向上取整需要 4 小时直接(10 3 - 1) // 3 12 // 3 4结果正确。二分区间为什么是[1, max(piles)]因为每小时最多只能吃掉一堆速度再快也不会快过“一堆一小时”所以 k 取最大值max(piles)时正好每堆一小时总时间是len(piles)。由于题目保证h len(piles)这个上界必然可行。如果你不确定题目有没有这个保证可以把右边界设成sum(piles)虽然会稍微增加二分次数但也完全没问题。4.3 二分模板的几个易错点第一左边界不能是 0。p 0时除零直接报错。第二二分条件要用feasible(mid)判断“是否够用”如果够用就往左收不够用就往右收。这个模板是标准的“寻找左边界”写法和“寻找右边界”的if nums[mid] target: left mid有点不同不要混。第三很多人会把hours h写成 h导致答案永远差一边界题直接 WA。一定要读清题目如果 h8正好 8 小时吃完是可行的是小于等于不是小于。5. 热门 100 题精选接雨水与滑动窗口最大值的双路口5.1 接雨水42从暴力到双指针接雨水是 LeetCode 热门 100 题里我用红色标记的经典因为它解法层次特别丰富暴力、前缀/后缀最大值、双指针、单调栈都能做而且每种解法背后的思维都能延伸到其他题。先说暴力每个格子能接多少水取决于它左右两侧最高柱子的较小值减去自身高度即ans min(leftMax[i], rightMax[i]) - height[i]但每格都扫左右两侧就是 O(n²)不可取。于是出现前后缀数组做法先用两个数组预处理每个位置左侧最高值和右侧最高值再扫一遍累计。这是 O(n) 时间、O(n) 空间思路最容易理解。双指针做法则是把空间压到 O(1)。核心观察是在左右两个指针相遇的过程中某一侧的高度更矮那么这一侧当前格子的水量就可以确定。因为水量取决于较低的一边。代码如下from typing import List class Solution: def trap(self, height: List[int]) - int: n len(height) if n 2: return 0 left, right 0, n - 1 left_max, right_max 0, 0 ans 0 while left right: if height[left] height[right]: if height[left] left_max: left_max height[left] else: ans left_max - height[left] left 1 else: if height[right] right_max: right_max height[right] else: ans right_max - height[right] right - 1 return ans这里有个容易绕晕的点为什么左侧柱更矮时只更新left_max而不需要考虑right_max因为右边有更高的柱子兜底当前格子的水位上限就取决于左侧已经扫过的最大高度。反之亦然。一旦理解“矮的一侧先结算”代码其实是水到渠成的。5.2 滑动窗口最大值239单调队列的精髓滑动窗口最大值是“固定窗口 求最值”的经典题。暴力写法是每滑动一次就遍历窗口内 k 个元素复杂度 O(nk)数据一大直接超时。更高效的做法是单调队列也就是在一个双端队列里维护一组“下标递增、值递减”的候选值。什么意思假设队列头永远是目前窗口最大值的下标当你加入一个新元素时从队尾弹出所有比它小的元素因为它们永远不可能是最大值了。这个操作保证队列里的值从队头到队尾是递减的。每次窗口移动时再把队头中已经滑出窗口的过期下标弹出。最后取队头就是答案。from collections import deque from typing import List class Solution: def maxSlidingWindow(self, nums: List[int], k: int) - List[int]: q deque() res [] for i, x in enumerate(nums): # 1. 清理过期下标 while q and q[0] i - k: q.popleft() # 2. 维护单调递减 while q and nums[q[-1]] x: q.pop() q.append(i) # 3. 窗口完整后开始记录答案 if i k - 1: res.append(nums[q[0]]) return res关键点是队列里存的是下标而不是值。为什么因为你要判断过期下标是不是已经滑出窗口只存值做不到这一点。这道题我一度以为自己写得很熟练结果有一次用了值存储加了个 while 循环去判断“当前最大值是否属于窗口”代码变得极其难看还出现了重复元素时误弹的问题。后来改成下标存储瞬间清爽。5.3 热门 100 刷题顺序的小建议既然这一章题目都来自热门 100我顺便说下我对“热门 100 怎么刷”的看法。很多人拿到 Top 100 之后从头到尾怼这样做效率很低因为题号就代表难度节奏并不是最优的学习顺序。我建议按照“题型”拆解先把 Hash 相关的题清掉因为它们是最好拿的送分题接着做数组、双指针、滑动窗口因为这类题套路明确然后做二叉树和 BFS/DFS这是面试出现频率最高的一块再做动态规划因为 DP 需要专门的时间啃最后挑战贪心、图论、字符串难题。接雨水和滑动窗口最大值正好处于双指针、队列、单调栈的交叉地带属于必须吃透的中等偏上题。如果你能把这两题背后的思维迁移到其他题上那 Top 100 后半程的很多所谓“困难题”其实也就是中等题的组合。6. 周赛 430现场编译与赛后复盘6.1 四道题的考场时间分配周赛 430 我这周也抽空打了一场。赛题记忆也许会褪色但那种考场节奏感很值得写下来。根据个人经验我把四道题分成这样几类题目预估难度目标用时核心考察点第一题Easy5 分钟内字符串/数组模拟边界条件第二题Medium15 分钟内排序 贪心部分前缀思想第三题Medium20 分钟前缀和、计数、双指针第四题Hard剩余时间状态设计或复杂结构我的实际安排是第一题绝不纠结直接写最暴力的逻辑前缀越界用哨兵处理第二题读完题先想贪心策略是否正确再动手否则退回去看数据范围第三题一般是最容易卡人的中等题因为它在普通模板上加了额外条件第四题如果 15 分钟内没有明确思路我会先写个暴力解保底至少拿部分分。这种“先打底分再抢高分”的策略在周赛场景下非常实用。真打周赛的人都知道不是每道题都要 AC保证前两题零失误、第三题拿下、第四题尽力就已经超过绝大多数参与者了。6.2 容易在周赛里翻车的细节周赛和每日一题最大的区别是没有题解给你提示时间压力还会放大你的低级失误。结合本周周赛我复盘出几个常见的翻车点第一读题时忽略数据范围。比如看到n 10^5还写了 O(n²) 的暴力直接 TLE。读题后应该立刻反应出时间和空间复杂度的量级这已经是刻进脑子的习惯。第二排序题里把比较器写反或者排序后没有考虑相等元素的顺序。第三三题里用了前缀和但没有考虑下标偏移差一位就是 WA。第四比赛最后一两分钟还在改代码结果把原本正确的版本改挂了。我现在的原则是如果比赛还剩下不到 10 分钟且当前代码已经通过样例那我宁可只加断言、做压力检查也不做大范围重构。6.3 赛后如何把一场周赛“吃干榨净”每次周赛结束后的 1 小时是学习效率最高的“黄金时间”。我会做三件事先把所有 AC 的代码按“最优解”重新写一遍因为周赛里你可能用了超长模拟赛后要对比讨论区更优雅的解法再把没 AC 的题翻题解但不是看完就关而是先尝试凭题解里的思路自己写 10 分钟最后把这场的四道题添加到个人题解库的“周赛”分类下并标注题型和错误原因。这样每次回看都很清晰。我见到很多人打完周赛就彻底放下隔两周回头看同样的题还是不会。这才是周赛最大的浪费。周赛是很好的“压力测试仪”它能暴露出你知识体系里的薄弱环节如果不能把这些反馈闭环到训练中去那周赛就只是打了一个寂寞。7. 本周刷题避坑手册实用速查这一周我至少踩了十几个细节坑很多都是可以提前预防的。我把它们整理成一张速查表方便你贴在手边场景典型错误正确姿势腐烂的橘子忘记特判fresh 0BFS 前先看有没有新鲜橘子没有直接返回 0腐烂的橘子在 BFS 内重新扫描全网格维护fresh计数器感染时减一基本计算器括号内符号被外层符号漏乘弹栈时用栈顶的 sign 乘以括号结果基本计算器空格导致字符判断失效i自增时跳过空格不单独处理空逻辑狒狒爱吃香蕉二分左边界取 0速度最小是 1避免除零狒狒爱吃香蕉hours h写成 h相等可行边界条件一定仔细读题接雨水用前缀/后缀数组时下标越界首尾单独赋值或直接用双指针滑动窗口最大队列里存值不存下标必须存下标才能判断窗口过期滑动窗口最大重复值被误弹用弹出旧的新值保留这些坑单独看都很小但组合在一起足以让你每一题都多调 10 分钟。我刷题时有一种感觉很多时候不是思路不对而是细节挡住了答题节奏。把这些坑提前记牢解题速度会有肉眼可见的提升。除了表格里的硬错误我还想强调一个心态问题。本周的题全是经典题导致评论区有很多人在“炫最优解”。你不必因为别人说“双指针秒了”就觉得自己笨。先能写出正确解法再逐步优化这个顺序永远是首要的。我自己回头看三年前的解法也常常被自己当时的丑陋代码逗笑但没有那段积累就不会有今天的清晰思路。8. 刷题指南如何长期坚持每日一题不烂尾8.1 每日一题的标准流程在坚持每日一题这件事上选择的方法比意志力更重要。我最推荐的标准流程是“20 分钟挑战法”拿到题目先自己思考 20 分钟如果思路清晰就直接写思路不清晰就去看提示或题解的开头部分看完后合上题解尝试独立写完。20 分钟后还是不会怎么办直接看题解但看题解也有讲究。不要只看最优解的最终代码而要看它的“思考路径”。很多人习惯于看完整题解然后照抄这样刷一百题都很难有本质提升。我会在看题解后做一个强制动作把题解页面关掉用自己的话说一遍思路再重新写代码。如果写不出来说明没真懂得再看一遍。如果某题你已经是会做的状态那也不要跳过试着换一种解法或者想想“这题和之前哪题相似”。比如你已经会了接雨水的双指针再想想单调栈怎么做已经会了腐烂橘子的 BFS再想想如何打印每一轮感染的橘子节点。这种“同一道题多种解法”的训练对提升代码灵活性帮助很大。8.2 建立个人题解库我强烈建议你在本地维护一个 Markdown 格式的题解库或者找一个支持标签功能的笔记工具。每一个题解记录下面至少要有四块内容题意摘要、自己的思路演进、最终代码、复盘备注。其中复盘备注写这次踩了什么坑以及下次再看时应该重点注意什么。这个题解库最大的作用是“记忆外置”。人的记忆是不可靠的过了两周你还能记得接雨水的双指针为什么左指针比右指针矮时结算吗大概率不记得了。但如果你当初写了一句“矮的一侧决定了当前格子的上界”再回来翻只需要 10 秒就能激活记忆。我现在的题解库已经积累了数百条面试前翻一遍自己的笔记比重新刷一遍线上题库快得多。8.3 时间管理不是每天都有 2 小时我知道很多人坚持不了每日一题不是因为懒而是因为每天抽不出完整时间。我的建议是把任务拆成两档工作日只保证“先做出最优解或搞懂一道题”周末再集中补题和重写题解。你不需要每天留出 2 小时哪怕只有 30 分钟也足够做出一道简单题或复盘一道中等题。另一个高效做法是“固定时间刷题”。我习惯把每日一题安排在早上因为它能激活大脑有人喜欢晚上刷认为那时思路更清醒。关键不在于哪个时间好而在于固定。到了某个时间点你就自动打开 LeetCode形成惯性后坚持的成本会急剧下降。8.4 哪些题值得二刷不是所有题都值得二刷。一道题是否值得进“二刷清单”我会看三个标准这道题是否用了超过两种算法思想是否存在我第一遍没想通的边界是否属于热门 100 或高频企业真题本周的几道题里我会毫不犹豫地把基本计算器和接雨水放进二刷清单因为前者细节太密后者有多种解法值得反复练习。二刷不是把答案背下来而是模拟“今天的我看到这道题能不能第一眼就判断出题型并写出优雅代码”。如果你能做到这一点说明这道题已经被你消化了可以把它从二刷清单移到“已掌握”分类。这个过程像滚雪球你的已掌握列表越来越长面试信心也会随之上升。9. 关于这一周我的个人体会这七天刷下来最大收获不是记住了某几个模板而是重新体会到了“经典题为什么经典”。腐烂的橘子教会我多源同步扩散怎么建模基本计算器教会我括号和符号的本质是上下文切换狒狒吃香蕉让我再一次确认二分答案的万能适用接雨水和滑动窗口最大值则是我反复拿出来对比双指针和单调队列的样本。这些题确实被无数人刷过但每刷一次我都能在细节上看到曾经忽略的东西。最后分享一个小技巧不要只在 LeetCode 网页上点“通过”就结束。我会在通过之后再把核心代码复制到自己的题解库并写一段“如果我是面试官我会追问什么”。比如腐烂的橘子面试官可能会问“如果烂橘子的扩散速度不是每分钟一格怎么办”基本计算器可能会问“如果增加乘法怎么办”。想清楚这些追问你的学习深度就不会停留在“AC 了”这个层面。希望这篇复盘对正在刷题的你有点帮助。后面我再遇到好的周会继续把“每日一题 周赛”展开写成总结咱们实战里继续见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑