news 2026/10/18 8:35:31

刷题复盘:字符串反转、滑动窗口与动态规划的共性思维与三大坑点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
刷题复盘:字符串反转、滑动窗口与动态规划的共性思维与三大坑点

1月15号,力扣刷题打卡照常进行。今天给自己排了三道题:一道字符串模拟、一道滑动窗口、一道二维动态规划,难度控制在简单加两个中等。字符串题考察的是边界细节,滑动窗口考察的是指针维护的节奏感,动态规划则是老生常谈的状态抽象能力。把这三类放到同一天练,我发现它们其实共享不少底层思维——窗口收缩和双指针翻转练的是同一套"谁先动、谁后动"的逻辑,动态规划的表格填充又和滑动窗口的右进左出有某种相似感。这篇文章就是把当天从读题、思路到写代码、调试的完整过程记录下来,再附上一些常规题解里不会写的坑点。刚开始刷题的朋友可以直接把代码拿去用,刷了一段时间的朋友可以参考里面的对比分析和优化路径。

1. 今天的刷题计划与选题思路

1.1 三道题怎么安排,各花多长时间

刷题这事,最怕的就是打开题库随机抽题,抽到什么刷什么,看起来努力,实际效率不高。我今天的安排是有意为之:先花两分钟扫一遍三道题,确认题型不重复,然后按"简单→中等→中等"的顺序推进,预计总时长控制在九十分钟以内。

实际用时记录一下:第一道字符串题从读题到 AC 用了 25 分钟,第二道滑动窗口 17 分钟,第三道动态规划 38 分钟,总共 80 分钟。为什么推荐这样固定时间?因为面试做题是有时间压力的,日常如果不对自己限时,到了现场很容易在某一题上死磕到底,搞崩整场节奏。我给自己定的规则是:简单题 15 分钟内必须出思路,中等题 30 分钟以内,超时直接看题解,然后把这道题标记为弱项,隔天再重做一遍。今天第一题超时了几分钟,问题出在练习题太少,后面会细说。

1.2 选题的逻辑:三种常见考法的底层联系

刷题要刷出效果,不能只刷数量,要刷类型。我把今天三道题放在一起,是因为它们分别代表了算法面试里三种最常碰面的能力维度:字符串模拟题考查的是"边界条件管理",滑动窗口考查的是"单调性和贪心直觉",动态规划考查的是"状态抽象能力"。

这三种能力不是孤立的。字符串原地翻转需要双指针,滑动窗口的收缩同样依赖两个指针的移动策略,状态转移方程的填表过程又和窗口维护有相似的递进关系。把相关联的题型放在同一天练,比一天刷五道同类题更能锻炼举一反三的能力。我见过很多刷了三百道题的人,遇到新题还是没思路,就是因为只记了题目答案,没有抽象出题型骨架。

2. 字符串模拟:反转单词背后的三个坑

2.1 题目要求与初步思路

今天第一道题是这样的:给定一个字符串,其中包含若干单词和空格,要求把单词顺序反转,同时去除多余空格——首尾空格去掉,单词之间的连续空格压缩成一个。举个例子:"the sky is blue" 要变成 "blue is sky the"。

读题之后第一反应是这题考"整体反转 + 局部反转":先把整个字符串反转一遍,这样单词顺序就反了,但每个单词内部字符也反了;然后再把每个单词内部反转回来,两步一组合,顺序正确。这个套路在字符串类题目里很常用,就像你先倒着读整句话,再把每个字正过来念,效果就是整句倒了但词没倒。不过这道题还加了"去除多余空格"的要求,所以不能只靠两次反转了事。

2.2 两种实现:代码少的不一定适合面试

第一种写法非常短,用语言自带的分隔和切片:

def reverseWords(s: str) -> str: words = s.strip().split() return ' '.join(words[::-1])

strip()去掉首尾空格,split()默认按任意空白字符切分,连续空格会被忽略,所以多余空格的问题也顺带解决了。这段代码跑测试用例毫无压力,如果你只是日常解题,这完全够用。

但面试场景下,这种写法可能被追问:如果要求空间复杂度 O(1),不准用额外的数组,你还能实现吗?所以还得会写手动版。Python 字符串不可变,要原地操作只能先转成字符数组。我的实现思路是:先把整个字符数组反转,然后从头部扫描,遇到单词就往前拷贝,单词前补一个空格,拷贝完立刻把这段单词局部反转,最后把多余部分截断。代码这样写:

def reverseWords(s: str) -> str: chars = list(s) n = len(chars) # 第一步:整体反转,让单词顺序反过来 chars.reverse() # 第二步:扫描每个单词,拷贝到前面并局部反转 storeIdx = 0 i = 0 while i < n: if chars[i] != ' ': # 在非首个单词前补一个空格 if storeIdx != 0: chars[storeIdx] = ' ' storeIdx += 1 j = i # 拷贝完整单词到 storeIdx 位置 while j < n and chars[j] != ' ': chars[storeIdx] = chars[j] storeIdx += 1 j += 1 # 反转当前单词内部字符 start = storeIdx - (j - i) chars[start:storeIdx] = list(reversed(chars[start:storeIdx])) i = j else: i += 1 return ''.join(chars[:storeIdx])

整体下来就是一次线性扫描,时间复杂度 O(n),空间上除了字符数组没有额外开销。理解这段代码的关键在于 storeIdx:它是写指针,记录的是"下一个要写入的位置"。扫描指针 i 负责找单词,写指针负责把单词搬回数组前端,搬完一个单词立刻反转它,这样单词内部顺序从"倒着"变回"正着",但单词之间的顺序维持了"倒着"。

2.3 踩过的三个具体坑

第一个坑是补空格的时机。我第一次写的时候,在每个单词前无条件加空格,结果首单词前面多出一个空格。正确做法是判断 storeIdx 不为 0,也就是当前已经写过至少一个单词,才补空格。这个判断要放在拷贝单词之前。

第二个坑是整体反转后,扫描逻辑容易混淆。因为chars.reverse()之后,字符串内容已经不是原来顺序,我从头扫描时见到的是"最后一个单词在前面的倒序",所以扫描顺序和拷入顺序必须匹配,容易绕晕。后来我用纸笔走了一遍样例才想清楚:扫描是从左往右,遇到单词就拷,这个单词本来就是原来的最后一个单词,拷出来之后局部反转一下,正好变成正序。

第三个坑是切片反转的边界。chars[start:storeIdx]这个切片的长度恰好是j - i,因为拷贝过程中 storeIdx 正好推进了j - i次。但如果你在拷贝循环里额外做了别的事导致 storeIdx 偏移,这里就会截错范围。调试时我发现切片末尾多抓了一个字符,当时就是因为在单词拷贝前先补了空格,却没把补空格的偏移算进去。正确写法是补空格发生在拷贝之前,storeIdx 先自增,之后拷贝的长度依然等于j - i,此时 start 用storeIdx - (j - i)计算,而不是用拷贝前的 storeIdx。

提示:字符串类题目最容易出错的地方不在算法本身,而在边界。建议写完代码后,用"空字符串""全空格""只有一个单词""单词间多个空格"四类用例快速自测,能挡掉大多数低级错误。

3. 滑动窗口:最短子数组的模板与易错点

3.1 问题建模与双指针移动逻辑

第二道题是:给定一个正整数数组 nums 和一个目标值 target,找出数组中满足总和大于等于 target 的长度最小的连续子数组,返回长度。如果不存在,返回 0。示例:target = 7,nums = [2,3,1,2,4,3],最短连续子数组是 [4,3],长度 2。

最直接的暴力做法是枚举所有起点和终点,两层循环把每个子数组的和算一遍,时间复杂度 O(n²)。这道题的数组长度可能到十万级别,O(n²) 肯定超时。能优化的地方在哪里?关键在于数组元素都是正整数,所以子数组的和随着右端点右移只增不减,具有一定的单调性。一旦某个窗口满足 sum >= target,右端点继续右移时窗口还在扩大,长度只会更长,没有继续保留的价值。因此,我们可以用两个指针维护一个动态窗口:右指针不断向右扩展,把新元素纳入窗口;当窗口和达到 target 时,记录当前窗口长度,然后左指针向右收缩,尝试找到更短的满足条件的窗口。

这个思路和日常生活中的"排队窗口"很像——队伍的右边有人不断进来,左边有人不断离开,我们只关心当前排队的人总和够不够,一旦够了就看看能不能少排几个人。

3.2 模板代码与复杂度分析

代码模板直接给出:

def minSubArrayLen(target: int, nums: list[int]) -> int: left = 0 cur_sum = 0 ans = float('inf') for right in range(len(nums)): cur_sum += nums[right] # 满足条件时收缩左边界 while cur_sum >= target: ans = min(ans, right - left + 1) cur_sum -= nums[left] left += 1 return 0 if ans == float('inf') else ans

这个模板几乎可以套用到所有"最短/最长满足条件的连续子数组"问题里。外层 for 是右指针,内层 while 在条件满足时收缩左指针。这里有一个细节值得注意:ans 的更新放在 while 循环内部。我在第一次写的时候,把ans = min(ans, right - left + 1)放在了 while 外面,导致记录到的不是满足条件时的最短窗口,而是收缩结束之后的窗口,结果全错。更准确地说,窗口满足条件的状态只出现在 while 的入口,所以必须在 while 内部先记录长度。

复杂度方面,虽然代码里有两个循环,但每个元素最多被 right 访问一次、被 left 移出一次,整体是 O(n) 时间,O(1) 空间。

3.3 变体:如果数组有负数会怎样

滑动窗口能工作的前提是数组元素为正(或非负),这样窗口和有单调性。如果 nums 里有负数,左指针收缩时窗口和不一定变小,右指针扩展时窗口和也不一定变大,left 和 right 的移动逻辑就失去了依据。这时候要换思路,常见方案是前缀和 + 二分查找,或者用单调队列维护窗口内的某种极值。本次题目明确给了正整数,所以不需要处理这种情况,但面试官可能会追问,提前想清楚为什么负数的存在会破坏模板,也是刷题的价值所在。

那遇到有负数的变体怎么排查?我习惯先写几个带负数的测试用例,观察模板的输出是否明显不符合预期,然后从"窗口和变化方向不可预测"这个根因出发,寻找新的数据结构。这种分析习惯比记住一堆题解有用得多。

4. 动态规划:网格路径的状态迁移

4.1 状态定义与边界推导

第三题是经典的网格路径问题:一个机器人位于 m 行 n 列的网格左上角,每次只能向下或向右移动一步,问到达右下角总共有多少条不同路径。

这道题用 DFS 当然可以做,但会重复计算大量子问题。动态规划的核心是用一个表格记录子问题的答案:定义 dp[i][j] 表示从左上角走到第 i 行第 j 列的不同路径数。机器人只能从上方格子向下走一步、或者从左方格子向右走一步来到达当前位置,所以状态转移方程是:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

边界条件:第一行的所有格子只能一直向右走,路径数都是 1;第一列的所有格子只能一直向下走,路径数也都是 1。把第一行和第一列初始化为 1,然后从 (1,1) 开始逐行逐列填充,右下角的 dp[m-1][n-1] 就是答案。

这个过程在草稿纸上画一个 3×3 的表格,手动填一遍,六十秒就能理解。填表顺序是"上一行先填完,当前行从左到右填",这保证任何一个格子在计算时,它依赖的 dp[i-1][j] 和 dp[i][j-1] 都已经有了确定值。

4.2 滚动数组优化

二维表格的完整空间复杂度是 O(m×n),但观察转移方程就会发现,计算每一行时只需要上一行的数据,而不需要更早的行。这就像你在看账本时只关心上一页的余额,没必要把前面所有页都背下来。因此可以只用一维数组滚动更新:

def uniquePaths(m: int, n: int) -> int: dp = [1] * n for i in range(1, m): for j in range(1, n): dp[j] = dp[j] + dp[j - 1] return dp[-1]

这里的 dp[j] 初始值代表第一行每个格子的路径数(都是 1)。进入第二行循环时,dp[j] 保存的是上一行第 j 列的值,dp[j-1] 因为刚被更新过,保存的是当前行左边格子的值,两者相加正好等价于 dp[i-1][j] + dp[i][j-1]。等这一行更新完毕,dp 就代表了当前行每个格子的路径数,下一行继续用这份数据滚动更新。这个优化把空间从 O(m×n) 降到 O(n),在 m 和 n 很大的时候收益非常明显。

4.3 从组合数角度验证答案

网格路径问题还可以用组合数学来理解:从左上角走到右下角,不管怎么走,总步数是 (m-1) + (n-1),其中向下走 m-1 步,向右走 n-1 步。路径的差异只在于"向下走"出现在哪几步,所以答案就是组合数 C(m+n-2, m-1) 或等价的 C(m+n-2, n-1)。

我在拿动态规划跑完结果后,又用组合数公式算了一遍,用于交叉验证。比如 m=3、n=3 时,动态规划结果是 6,组合数 C(4, 2) 也是 6,两边对上了,代码基本没有错。不过要注意,直接用组合数公式计算时中间结果可能很大,需要处理溢出;动态规划用加法逐层累加,天然避开了这个麻烦,所以日常刷题我更推荐先写动态规划,再用组合数做验证。

5. 复盘:调试技巧与通用心得

5.1 今天的排错全记录

三道题都不是一遍过的,记录一下实际遇到的典型问题,按题目整理成速查表:

题目遇到的问题排查思路与解决
字符串反转单词间补空格导致首单词前多出空格加 storeIdx != 0 判断,区分是不是第一个单词
字符串反转局部反转的切片边界多抓一个字符明确 start 应基于拷贝后的 storeIdx 计算
滑动窗口ans 记录成了收缩后的长度把长度更新移到 while 循环内部,写在 cur_sum -= num[left] 之前
动态规划滚动数组初始化错了行方向手动模拟 3×3 填表,确认 dp 数组更新顺序与行扫描一致

这里想多说一句:定位 bug 最有效的手段不是反复读代码,而是构造极端用例。今天我就是在全空格字符串的用例上发现第一题的补空格逻辑漏了判断,又在 target 大于数组总和的用例上发现第二题float('inf')的兜底返回值没问题。如果你写完代码先空想逻辑,再拿三五个手工用例跑一遍,基本能覆盖八成错误。

5.2 刷题节奏和心态调整的经验

刷题不是刷得越多越好,每天都刷同样的题型容易陷入舒适区。我现在的方法是每周一三五练基础数据结构(字符串、数组、链表),二四练算法思想(二分、双指针、动态规划),周末随便挑一两道之前标记的错题重做。今天正好踩中一个新问题:字符串第一题在 15 分钟没出完整思路,心里有点急,硬着头皮死磕,结果 25 分钟才写完。复盘时发现,这种题应该拆成两个子步骤来看——先不考虑空格压缩,把反转做好,再加空格处理,而不是一上来就想一步到位。把复杂问题分步拆解的思维,本身就是刷题要练的内功。

5.3 一个可以长期用的自查清单

结合今天的实战,我整理了一个自查清单,每次提交前照着过一遍:

  • 输入为空或长度为零时,代码是否报错?
  • 边界值(第一个元素、最后一个元素)是否被正确处理?
  • 连续重复输入、全相同输入是否会导致死循环?
  • 指针或索引在循环结束时是否可能越界?
  • 答案变量初始化值是否足够大/足够小,能否区分"无解"和"有解"?
  • 空间优化后,旧数据是否可能被新数据提前覆盖?

这份清单花不了两分钟,但能挡住大量低级失误。我最近一个月按照这份清单检查,线上做题的通过率明显提升,更重要的是做错之后返工的时间少了。

最后再分享一个小技巧:刷完一道题后,不要急着点下一题,先花两分钟在题解的评论区或者自己的笔记里写一句"这题的核心套路是什么,和哪道旧题类似"。坚持一个月,你会发现脑子里慢慢形成一张题型网络,再遇到新题时,会本能地问自己:这题该套双指针、二分还是动态规划?这比刷题数量更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:38:04

古典密码习题破解指南:凯撒、维吉尼亚、希尔密码与Python实现

看到「应用密码学&#xff08;第三版&#xff09;第二章习题答案」这个标题&#xff0c;我知道你多半不是想抄答案&#xff0c;而是被第二章那些古典密码题折磨过。凯撒、单表替换、维吉尼亚、希尔、转置&#xff0c;题型看似只是加密和解密&#xff0c;可真到作业题里&#xf…

作者头像 李华
网站建设 2026/10/17 22:13:01

Postman实战指南:接口生命周期管理与团队协作协议

1. 这不是“接口测试工具说明书”&#xff0c;而是一份真实项目里反复打磨出来的Postman实战手记我第一次在某电商后台系统做接口联调时&#xff0c;被三个环境变量来回切换搞到凌晨两点——开发说“接口在dev环境跑通了”&#xff0c;测试说“uat环境返回500”&#xff0c;运维…

作者头像 李华
网站建设 2026/10/17 4:24:08

雪消融算法优化SVR:MATLAB实现与参数自动寻优

做预测模型这些年&#xff0c;我几乎隔几天就会遇到同一个问题&#xff1a;SVR&#xff08;支持向量回归&#xff09;的预测效果怎么时好时坏&#xff1f;说句实在话&#xff0c;大部分情况下模型本身没有错&#xff0c;错的是惩罚系数C和核函数参数gamma靠拍脑袋定。SVR对这两…

作者头像 李华
网站建设 2026/10/14 5:26:45

企业AI工具被封后:统一网关、账号治理与多模型备份实战

最近一个月&#xff0c;我接到了好几个团队负责人的求助&#xff0c;开场白基本是同一个模子&#xff1a;周五还好好的&#xff0c;周一上班发现团队里的GPT账号集体登不进去&#xff0c;Claude这边也弹了风控提示。紧接着&#xff0c;工作群里开始有人转发各种来路不明的"…

作者头像 李华
网站建设 2026/10/15 10:50:31

非生成式AI决策模型:万亿Token级确定性判断技术

1. 项目概述&#xff1a;当AI不再“说话”&#xff0c;它在做什么&#xff1f;“不生成文本的AI日吞一万亿Token”——这句话刚看到时&#xff0c;我手里的咖啡差点洒出来。不是因为夸张&#xff0c;而是因为它精准戳中了当前大模型生态里一个被集体忽视的真相&#xff1a;我们…

作者头像 李华
网站建设 2026/10/15 9:14:27

软件工程复习全攻略:从期末冲刺到考研体系化备考指南

软件工程这门课&#xff0c;说难不难&#xff0c;说简单也真不简单。期末临时抱佛脚背两天&#xff0c;考完就忘&#xff1b;考研复习又得从零捡起来&#xff0c;跟没学过似的。我见过太多同学在这门课上栽跟头&#xff0c;不是因为笨&#xff0c;而是压根没搞明白它到底考什么…

作者头像 李华