资讯动态

力扣刷题实战:滑动窗口、动态规划与DFS的优化思路

发布时间:2026/10/9 0:24:22 来源:尧图企业网站定制
2026年1月14日照惯例打开力扣准备刷两道题保持手感结果一坐就是两个多小时。今天的计划本来是热题100里的随缘三题最后却意外地把无重复字符的最长子串、最长回文子串、岛屿数量这三道经典题从暴力到最优解完整梳理了一遍。这篇刷题笔记不打算写成标准题解而是想记录一下我实际做题时的思考顺序、写代码时踩的坑以及面对超时之后完整的排查链路。如果你是刚开始刷力扣或者刷了一段时间还在暴力解徘徊这篇文章应该对你有参考价值。1. 今天的选题逻辑为什么我总从热题100里挑题1.1 热题100不是用来“刷完”的是用来“反复刷”的很多人拿到力扣热题100就定了个目标一天五题二十天刷完刷完就算完成任务。但按我自己的经验这种刷法基本属于自我感动——今天看完题解的五道题下周再见到连思路都记不起来。我今天的选题思路很简单从热题100里挑三道平时不怎么碰的类型每道题都给自己设定一个必须写出两种以上解法的硬指标。力扣热题100之所以值得反复刷是因为它基本就是面试出题频率的浓缩样本。这里面没有太多偏题怪题每一道题对应的都是某种高频算法套路。我今天选的三道题——无重复字符的最长子串滑动窗口、最长回文子串中心扩展/动态规划、岛屿数量网格DFS——恰好覆盖了三种完全不同类型的思维模型把它们放在同一天刷能明显感觉到思路切换的乐趣。1.2 我给自己定的刷题节奏这里分享一套我最近一直在用的节奏今天实践的体验也印证了这套方法是有效的拿到一道题先花10到15分钟独立思考不翻题解、不查讨论区。想不出来就硬想想到什么写什么哪怕是最蠢的暴力解法也要先写出来。暴力解法通过之后要求自己继续优化。这一步是很多人忽略的——题解写完一遍就跑了但其实从暴力到优化的过程才是最能长本事的地方。如果15分钟过去还是一点思路都没有才去看题解。看题解不是看代码而是看别人的“第一次跳跃”是怎么发生的为什么他想到用滑动窗口而不是前缀和为什么这题用中心扩展而不是DP当天或者隔天一定要把新题重新默写一遍不许看代码。今天这三道题里无重复字符的最长子串和岛屿数量是我能独立写出来的最长回文子串的中心扩展法我一开始想的其实是DP后面详细对比了两种思路的差异。2. 无重复字符的最长子串滑动窗口从“能过”到“优雅”的两次进化2.1 题目回顾与典型的暴力误区题目描述很简洁给定一个字符串找出其中不含重复字符的最长子串的长度。比如s abcabcbb最长的不含重复字符的子串是abc长度是3。我第一反应想到的其实是暴力。直接枚举所有起点和终点对每个子串用set检查一下有没有重复字符有就跳过没有就更新答案。看起来没问题但这个写法的复杂度是灾难级的枚举起点O(n)枚举终点O(n)每次还要构建set确认整个子串是否重复又需要O(n)整体加起来是O(n^3)。class Solution: def lengthOfLongestSubstring(self, s: str) - int: n len(s) ans 0 for i in range(n): for j in range(i, n): substring s[i:j 1] if len(set(substring)) len(substring): ans max(ans, len(substring)) return ans这个解法在力扣上对应一个中等长度用例就会超时。为什么很多新手会下意识写出这种代码因为枚举所有子串再逐个检查是最符合直觉的思路它不需要任何抽象建模。我以前刷题最大的教训就是看到字符串子串问题第一步就该怀疑暴力枚举行不行不是所有枚举都能靠计算机算力硬扛过去的。2.2 第一版优化双指针加哈希集合让left只往右走暴力解法之所以慢是因为它在反复检查已经检查过的内容。比如你检查了abc没重复下一个窗口abca时你又重新把a、b、c全部过了一遍。滑动窗口的思路就是把这个重复劳动去掉窗口左指针left和右指针right维护当前考察的子串范围右指针每次扩展一个字符如果新字符导致窗口内有重复就让左指针不断收缩直到窗口重新合法。class Solution: def lengthOfLongestSubstring(self, s: str) - int: window set() left 0 ans 0 for right, ch in enumerate(s): while ch in window: window.remove(s[left]) left 1 window.add(ch) ans max(ans, right - left 1) return ans这套代码的关键点有两个。第一个是while ch in window这个内层循环它保证了窗口在每一步结束时都是合法的但不保证最短——因为left只会在窗口出现重复时才会移动而且每次只挪一格。第二个是ans max(ans, right - left 1)必须放在window.add(ch)之后因为只有把当前字符加进去之后窗口的右边界才是right长度计算才正确。这个版本的复杂度已经降到O(n)每个字符最多进入window一次、离开一次。这个复杂度优化是怎么来的关键不在于用了set而在于left指针只增不减整个算法对外层循环的每次迭代来说内层while的总执行次数不会超过n次。2.3 第二版优化用哈希表记录下标left直接跳转上面的写法已经能通过所有用例了但我在看别人的题解时发现还能进一步优化与其用while循环一步一步地挪left不如用哈希表记录每个字符最近一次出现的位置遇到重复字符时直接把left跳到那个位置的下一个。class Solution: def lengthOfLongestSubstring(self, s: str) - int: last {} left 0 ans 0 for right, ch in enumerate(s): if ch in last: left max(left, last[ch] 1) last[ch] right ans max(ans, right - left 1) return ans这一版代码更短但容易写错的地方也更隐蔽。我一开始写的时候习惯性写成left last[ch] 1结果跑用例时发现left可能往左弹回去。举个例子s abba遍历到最后一个a时last[a]是0如果直接left 1那窗口从1到3表示bba这显然错了——因为left已经因为b的重复移到了2不能再回头。所以这里必须left max(left, last[ch] 1)保证左指针永远是单调的。这段代码还有一个天然的边界适配字符串为空时直接返回0字符串全部字符都不重复时left一直是0ans正确地累计为n完全不用特殊处理。这也是我喜欢用这个版本的原因——边界条件蕴含在逻辑里而不是靠额外的if去维护。2.4 两种滑窗写法的选择依据用set版本适合在面试的时候讲因为它和滑动窗口这个概念的对应关系更直接面试官容易跟上你的思路。用哈希表版本更适合实际刷题和比赛代码更短常数也更小。如果你准备面试我建议两个版本都掌握并且能说清楚为什么第二个版本里left要用max保护——这是面试官最爱追问的一个点。另外子串和子序列是两个不同的东西子串必须是连续的这套指针移动的写法对子序列无效别搞混。3. 最长回文子串中心扩展法与动态规划的正面交锋3.1 这题我一开始是怎么想的动态规划今天第二题是力扣第5题最长回文子串。给定字符串s要求返回其中最长的回文子串。回文就是正读反读都一样比如babad里最长的回文子串是aba或bab长度都是3。我一开始思考的方向是动态规划因为子串满足某种性质这类问题是DP的经典应用场景。定义dp[i][j]表示s[i:j1]是不是回文状态转移其实很简单s[i] s[j]且内部dp[i1][j-1]为真时dp[i][j]为真。长度1和长度2的子串要单独初始化。class Solution: def longestPalindrome(self, s: str) - str: n len(s) if n 2: return s dp [[False] * n for _ in range(n)] start, max_len 0, 1 for i in range(n): dp[i][i] True for length in range(2, n 1): for i in range(n - length 1): j i length - 1 if s[i] ! s[j]: continue if length 2 or dp[i 1][j - 1]: dp[i][j] True if length max_len: start, max_len i, length return s[start:start max_len]这个写法在时间复杂度O(n^2)上是达标的但空间复杂度也是O(n^2)。对于长度几万的字符串这个二维数组的开销就不容忽视了。我记得之前刷某个变种题时就是因为没优化空间直接被卡了内存。3.2 换思路回文天然适合从中心向外扩展后来我换了个角度想回文串的本质是“中心对称”。那么找最长回文子串不如直接从每一个可能的对称中心向外扩展看看能扩多远。一个长度为n的字符串中心一共有2n - 1个因为奇数长度的回文中心是一个字符偶数长度回文的中心是两个字符之间的缝隙。中心扩展法的实现非常简洁class Solution: def longestPalindrome(self, s: str) - str: def expand(left: int, right: int) - str: while left 0 and right len(s) and s[left] s[right]: left - 1 right 1 return s[left 1:right] ans for i in range(len(s)): odd expand(i, i) even expand(i, i 1) ans max(ans, odd, even, keylen) return ans这个写法最妙的地方在于奇数中心和偶数中心被统一成了同一个expand函数只是传参不同。expand(i, i)处理的是aba这类中心落在b上expand(i, i 1)处理的是abba这类中心落在b和b的缝隙里。我写这个函数时踩了一个小坑while循环退出后left和right已经越界或者指向了一对不等的字符此时回文区间是[left1, right-1]而Python切片的右边界是不包含的所以直接返回s[left1:right]是对的。这个边界我想了两分钟才完全确定建议你写完后专门用一个边界用例验证一下比如字符串只有一个字符的情况。3.3 DP和中心扩展的选型逻辑从复杂度上看中心扩展法时间是O(n^2)空间O(1)比DP的O(n^2)空间要好得多。DP的优势在于可扩展性——如果题目问的是回文子串的数量之类需要记录每个子串结果的场景DP就顺手了。在实际面试中如果遇到回文类的题目我现在的倾向是先说DP思路因为容易讲清楚状态定义再补一句其实这题还能用中心扩展法把空间复杂度降到O(1)然后直接写中心扩展。这样既展示了扎实的DP功底又体现了优化意识。3.4 一个小技巧用max的key参数省掉if判断第三段的代码里我用了一句ans max(ans, odd, even, keylen)来同时比较三个字符串的长度。这个写法比if len(odd) len(ans): ans odd这种逐段更新要简洁得多。不过要注意keylen不能丢否则Python默认按字典序比较字符串b会排在abc前面整个逻辑就错了。这类细节是我实际写代码时最喜欢用的省力技巧但前提是你能讲清楚它的比较规则不然面试时被追问反而露怯。4. 岛屿数量网格题里的DFS细节决定成败4.1 题目描述一个二维网格里的连通块问题第三道题是力扣第200题岛屿数量。输入是一个二维的grid里面只有1和0两种字符1代表陆地0代表水要求统计陆地块的数量。比如示例里有两个孤立陆地答案就是2。所谓岛屿就是上下左右四个方向连成一片的陆地集合。拿到这题我第一反应是DFS。网格本身就是一种图的表示每一格是一个节点上下左右的邻居是它的边。找岛屿数量本质上就是找图里有多少个连通分量。DFS的思路是遍历每个格子遇到1说明发现了一个新的岛屿计数加一然后从这个格子开始递归地把所有相邻的1都淹没成0这样后面遍历到它们时就不会重复计数。4.2 为什么可以放心地原地修改数组最开始我看到题解里直接grid[i][j] 0还有点顾虑担心修改了原数组会不会影响后续遍历。后来想明白了这是DFS里最常见的访问标记策略只是这个题巧妙的点在于题目本身没有说不能改grid而且把一个岛屿全部标记成水后这些格子对最终答案的贡献已经结算完了后面再碰到它们也不可能计入新的岛屿所以原地修改是安全且高效的。如果不允许修改原数组那就要额外开一个visited数组代价是O(mn)的空间。4.3 递归写法与方向数组的两个细节from typing import List class Solution: def numIslands(self, grid: List[List[str]]) - int: if not grid: return 0 m, n len(grid), len(grid[0]) directions [(1, 0), (-1, 0), (0, 1), (0, -1)] def dfs(i: int, j: int) - None: if not (0 i m and 0 j n) or grid[i][j] 0: return grid[i][j] 0 for di, dj in directions: dfs(i di, j dj) ans 0 for i in range(m): for j in range(n): if grid[i][j] 1: ans 1 dfs(i, j) return ans我写这段的时候有两个细节想多说一句。第一个细节是边界判断的顺序。not (0 i m and 0 j n) or grid[i][j] 0这一句里的两个条件顺序不能反。必须先判断坐标是否在范围内再访问grid[i][j]否则当i -1时会抛出索引越界异常。Python的or是短路求值的前面的条件为真就不会执行后面的表达式所以把越界判断放前面是安全的。第二个细节是方向数组的写法。我习惯用directions列表统一管理四个方向然后循环递归。有些人更喜欢写四次dfs(i1, j)、dfs(i-1, j)在只有四个方向时两种写法都行但如果你要改成8个方向再加上对角线方向数组的扩展性优势就出来了。我自己在代码里用方向数组还有一个心理层面的好处写循环不会漏方向。4.4 大网格下的递归深度风险网格DFS有一个需要警惕的问题递归深度。Python默认的递归深度限制大约是1000层而一个几百乘几百的网格DFS完全可能沿着一条路径递归几百上千次。如果题目给的数据范围特别大递归写法可能会直接爆栈报RecursionError。遇到这种情况我建议把DFS改成显式栈或者直接用BFS。用栈模拟DFS的核心就是用列表存待访问的节点循环里pop出来处理只是要注意入栈的顺序会影响遍历方向。其实对于岛屿数量这道题BFS和DFS在复杂度上没有区别都是O(mn)选哪个主要看你的习惯。我平时在这个题上优先写DFS因为它代码更短、更直观但我会在心里记着如果递归爆栈了马上切BFS这个备用方案。5. 一次真实的超时排查从O(n^3)到O(n)的完整链路5.1 问题现象明明逻辑没问题就是过不了今天刷无重复字符的最长子串时我故意先提交了最早写的那版暴力代码想看看力扣到底会不会超时。结果毫无悬念地看到了Time Limit Exceeded测试用例是一个几千字符的长字符串。这种报错特别让人烦躁因为不是答案错误那种直白的问题——你的逻辑是对的但太慢了系统不让你过。我把这个常见的刷题痛点单独拿出来说是因为很多新手看到TLE就慌了开始怀疑自己的解法里面有没有隐藏bug在错误的方向上反复调试。正确的做法是TLE基本上等于告诉你算法的复杂度级别撑不住数据范围第一步应该老老实实估算复杂度。5.2 第一步先算复杂度确认瓶颈在哪暴力版最外层两层循环枚举所有子串内层还要构建set整体是O(n^3)的复杂度。假设字符串长度是5000那最坏情况要执行约1250亿次基本操作这在力扣的时间限制下是不可能跑完的。一旦确认复杂度级别是主要矛盾优化的方向就很明确了必须消除重复检查。5.3 第二步逐步优化并验证每一步我从暴力版出发改造过程是这样的把第二层循环里构建set的检查去掉改成边移动右指针边维护一个哈希集合。右指针每前进一步先检查当前字符是否在集合里在的话就移动左指针直到不在。这就是前面写过的第一个滑窗版本复杂度降到了O(n)。提交后发现用例通过了但我觉得while循环频繁调用window.remove还是有些多余的操作。于是进一步用哈希表记录每个字符最近的出现下标让left一次跳到位。复杂度依然是O(n)但常数更小了。每次优化完我都跑一遍原来超时的那个用例确认从TLE变成了AC。这个优化一步、验证一步的习惯非常重要因为有时候你以为在优化实际上却引入了新的bug。有了原超时用例作为回归测试每次改动有没有损害正确性就一目了然。5.4 第三步总结这类问题的通用排查套路TLE排查其实有一条通用的链路我今天走了一遍之后觉得可以沉淀成固定步骤看数据范围掐指一算当前解法的复杂度和该题期望的复杂度级别对照。找到复杂度里最贵的一个环节——通常是重复扫描或重复计算——思考有没有办法用空间换时间比如哈希表记录历史信息。小规模测试用例不能帮助你发现超时问题要用接近上限的数据量来复现TLE。优化完成后保留一个最大规模的用例做回归验证。这条链路不但适用于字符串题对数组、链表、树上的各种超时问题都一样好用。遇到TLE别慌先算复杂度再定位热点最后针对性优化。这种思维方式比记住某个具体题的优化方法要值钱得多。6. 复盘热题100今天的题型规律和我调整后的刷题节奏6.1 热题100的题型分布观察刷完今天这三道题我又把热题100的题目列表翻了一遍按自己的理解做了个大致的题型归纳。这个分布不是官方统计是刷了几十道之后的体感判断题型类别大致占比我今天遇到的相关题目数组与哈希表15%无重复字符的最长子串哈希表版本双指针与滑动窗口10%无重复字符的最长子串滑窗版本二叉树相关15%——图与搜索DFS/BFS10%岛屿数量动态规划15%最长回文子串DP版本回溯算法10%——链表、栈、队列15%——贪心与其他10%——这个分布告诉我一个事热题100里真正难的并不是某一个具体的算法而是同一道题往往可以被归类到多个类别里。比如无重复字符的最长子串既是哈希题又是滑动窗口题最长回文子串既可以是DP题又可以是中心扩展题。所以刷题时不要满足于AC了就完事试着给自己的解法打多个标签能横跨的类型越多你对算法套路之间联系的理解就越深。6.2 今天暴露出来的薄弱点今天这三道题暴露了我的两个问题。第一个是DP的状态定义还是不够快。最长回文子串我虽然能写出DP解法但花了不少时间在确认状态转移方程的细节上尤其是长度2的初始化和长度从短到长的遍历顺序如果思路不清晰很容易在循环边界上写错。第二个是涉及到二维数组的边界判断容易想当然岛屿数量里的越界检查顺序我一开始就写反了是测试用例报错才发现的。根据今天的情况我调整了一下之后的刷题计划每周固定留两天专门做DP题每次至少两道所有二维网格类的题目必须在一开始就把边界判断条件想清楚再动手可以先在草稿纸上画一个3乘3的示意图。6.3 我在用的几个刷题管理技巧最后分享几个今天实践下来觉得很实用的技巧每道题在提交通过之后哪怕代码已经AC我也会在讨论区扫一眼看看有没有更短的写法或者更高级的思路。今天最长回文子串我看到了一个Manacher算法虽然O(n)复杂度很诱人但实现的细节实在多我把它标记为一周后再回来做一次不急着当天硬啃。给每道题在笔记里写一行一句话思路。无重复字符的最长子串就总结成右进左缩哈希记下标下次翻到笔记时看到这一句话就能快速回忆起核心思想。如果一道题看了题解才写出来我会在隔天再默写一遍。今天的三道题里最长回文子串的中心扩展法算是看题解后重写的所以明天第一件事就是不看代码再写一次。刷题这东西瓶颈往往不在智商而在方法。今天这一天虽然只弄了三道题但每道题都揉开了、掰碎了从暴力到优化再到边界处理都过了几遍收获比之前一天囫囵吞五道题要大得多。如果你刷题也刷到瓶颈期不妨试试把速度放慢一点把每一道题从会写变成能讲清楚为什么这么写手感会完全不一样。

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

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

免费获取报价 →
↑