news 2026/10/11 7:20:44

数组刷题Day2:滑动窗口与循环不变量实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数组刷题Day2:滑动窗口与循环不变量实战解析

跟着代码随想录刷题,Day2这天我特意没急着往下开新专题,而是把数组部分的两道硬题拿出来重新啃了一遍:LeetCode 209长度最小的子数组、LeetCode 59螺旋矩阵II。代码随想录跟别的刷题资料最大的不同,在于它会把同一类解法的题目集中到一起,用一套模板串起来讲,但Day2这个位置恰恰是很多人容易卡住的地方——前面的二分查找、移除元素还没完全消化,突然就要求上滑动窗口和循环不变量,节奏一快心态就容易崩。这篇就用我的真实刷题过程,把这两道题的思考链路、代码细节、边界条件全部过一遍,顺带聊聊我在实测中踩过的坑,给同样跟这个系列刷到Day2的朋友做个参考。

1. Day2的核心目标与前置心法

1.1 为什么数组刷题绕不开滑动窗口和循环不变量

数组类题目看起来简单,实际上手最容易翻车的就是边界处理。暴力解法往往“思路一分钟,超时两行泪”,一上优化又容易被各种区间定义绕晕。代码随想录把Day2落在两道经典题上,本质上就是逼你掌握两个极其重要的编程思维:滑动窗口和循环不变量。

滑动窗口解决的是“连续子数组”问题,核心思路是让右指针不断扩展,左指针按需收缩,让窗口始终保持某种性质。循环不变量解决的是“按规则填充矩阵”这类问题,核心思路是让每次循环的区间边界定义保持一致,不因为圈层变化而临时改规则。这两个思维在后面的链表、字符串、模拟题里都会反复出现,Day2不把它们吃透,后面只会越欠越多。

1.2 前置知识自查清单

我在正式开始做题前,先对照着检查了一遍基础知识点,这里也分享出来,你可以直接拿来自测:

  • 数组下标从0开始,循环里最容易被忽略的就是边界到底取不取等号。
  • 双指针思想需要熟练掌握,尤其要理解“快慢指针”和“左右指针”的区别。
  • 时间复杂度基本概念要清晰,至少能判断出O(n²)和O(n)在数据规模达到10⁵时的实际差距。
  • vector二维数组的初始化方式要熟练,写螺旋矩阵时如果初始化都手抖,后面很难专注在逻辑上。

这些前置内容不要求你会什么高级数据结构,但如果你连while和for的边界条件都靠猜,那Day2确实会有点吃力。我自己习惯把刷题前置知识控制在半小时内复习完,不要恋战,直接进题目实战,遇到不会的再回头查。

2. 长度最小的子数组:滑动窗口不是“高级优化”,而是必然结论

2.1 暴力解的问题出在哪里

先看题目要求:给定一个正整数数组和目标值target,找出数组中满足其和大于等于target的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回0。

我第一次做这道题时,第一反应是双重循环枚举所有起点和终点,计算每个子数组的和,然后更新最小值。这个思路完全正确,但问题在于复杂度是O(n²),在数组长度达到10⁵量级时,超时几乎是必然的。

为什么说滑动窗口在这里不是“高级优化”而是“必然结论”?因为题目里有个非常关键的性质:所有元素都是正整数。这意味着数组具有单调性——子数组越长,累加和越大。基于这个性质,一旦右侧扩展时发现当前窗口和已经大于等于target,那就说明以当前左指针为起点的所有子数组里,当前这个窗口已经是最短的候选了(再往后更是浪费),所以可以放心收缩左边界,而不是继续枚举后面的右指针。这个“充分利用单调性、及时止损”的过程,就是滑动窗口的朴素来源。

2.2 滑动窗口代码实现与逐行解析

我最终写出的C++解法如下,这个写法也是跟着代码随想录的模板思路调整过的:

class Solution { public: int minSubArrayLen(int target, vector<int>& nums) { int n = nums.size(); int left = 0; int sum = 0; int ans = n + 1; // 初始化一个不可能达到的大值 for (int right = 0; right < n; right++) { sum += nums[right]; // 右指针扩张,把新元素纳入窗口 while (sum >= target) { // 窗口满足条件时尝试收缩 ans = min(ans, right - left + 1); // 记录当前窗口长度 sum -= nums[left]; // 把左指针指向的元素移出窗口 left++; } } return ans == n + 1 ? 0 : ans; } };

逐行拆解一下子容易记混的地方:

ans初始化为n + 1而不是nums.size(),因为如果整个数组正好全部元素加和才能超过target,那合法答案最大就是n,所以n + 1天然代表“没找到”的状态,最后通过三元表达式输出0。

for循环里,right作为窗口的右边界不断前进,每次只增加一个元素。while循环负责收缩,在这里面left才移动。很多人容易忘记的是:收缩不是只在sum大于target时做一次,而是要收缩到sum < target为止,因为收缩一次窗口后,新的窗口可能依然满足条件,还得继续收缩,直到不满足为止。

还有一处细节:right - left + 1才是窗口长度,闭区间[left, right]的长度计算公式相当容易写错。如果只写right - left,那长度就会少算1,最终答案很可能偏小。

2.3 复杂度分析与边界用例实测

这个解法的时间复杂度是O(n),因为left和right都最多移动n次,每个元素最多被加入一次、移出一次。空间复杂度O(1),没有额外数组。

我实测下来,最容易翻车的几个测试用例:

  • target = 7, nums = [2,3,1,2,4,3],正确答案是2(子数组[4,3])。如果你没在while里持续收缩,可能会得到3甚至4。
  • target = 11, nums = [1,1,1,1,1,1,1,1],所有元素加起来都不到target,应该返回0。这种情况很考验ans的初始化逻辑,如果初始化为INT_MAX但没有最后的ans == n + 1判断,就会直接输出一个垃圾大数。
  • target = 1, nums = [1],单个元素自身就满足条件,答案是1,此时窗口逻辑要确保右指针走完之前能正确记录长度。

3. 螺旋矩阵II:循环不变量帮你一句话碾碎边界条件

3.1 为什么这种题最容易毁在“最后一个元素”

螺旋矩阵II要求把1到n²的数字按顺时针螺旋顺序填入n×n矩阵。说实话,这题思路本身不难:一圈一圈往内填,每圈四条边各填一层。但真正动手写代码时,很多人会陷入边界条件的地狱——明明填充到右上角了,中间某个格子又填重复或者漏填。

根本原因在于区间定义不统一。上一圈你写的是左闭右开,下一圈不小心写成左闭右闭,然后四条边交接的角落就会出错。

代码随想录里反复强调循环不变量,在这里具体体现为:每一圈的每一条边,都统一采用左闭右开区间来处理。即每条边都保留最后一个元素给下一条边去处理,四条边转完一圈,正好不重不漏。这一句话就能省掉90%的边界死磕。

3.2 循环不变量写法与代码实现

按照“每圈四条边全部左闭右开”的思路,我实现的C++版本如下:

class Solution { public: vector<vector<int>> generateMatrix(int n) { vector<vector<int>> matrix(n, vector<int>(n, 0)); int startX = 0, startY = 0; // 每圈的起始位置 int offset = 1; // 每圈边界收缩量 int count = 1; // 要填入的数字 int loop = n / 2; // 总圈数 while (loop--) { int i = startX, j = startY; // 上边:从左到右,左闭右开 for (j = startY; j < n - offset; j++) { matrix[startX][j] = count++; } // 右边:从上到下,上闭下开 for (i = startX; i < n - offset; i++) { matrix[i][j] = count++; } // 下边:从右到左,右闭左开 for (; j > startY; j--) { matrix[i][j] = count++; } // 左边:从下到上,下闭上开 for (; i > startX; i--) { matrix[i][j] = count++; } startX++; startY++; offset++; } if (n % 2 == 1) { matrix[n / 2][n / 2] = count; // 奇数阶中心元素 } return matrix; } };

关键点在于理解j和i在循环之间是如何天然衔接的。第一个for结束后,j停在了n - offset这个位置,它正是右边这一列第一个要填充的行位置;第二个for结束后,i停在了n - offset,又刚好是下面这一条边从右往左填充的起始列位置。四条边首尾相接,靠的正是循环完毕后变量已经停在正确坐标上,不需要额外修正。

每次圈数增加,startX和startY都要往内收缩一格,同时offset加1,表示每边的有效填充范围进一步缩短。这样当n为奇数时,最中间会剩下一个格子,用matrix[n/2][n/2] = count;单独处理;当n为偶数时,所有格子都会被完整填满。

3.3 实测几个n值的正确性验证

我写完代码后,习惯性先跑几个小规模n值确认行为:

  • n = 1,直接走奇数分支,中心填1,输出[[1]]。
  • n = 2,走一圈循环,四条边各填一个元素,得到[[1,2],[4,3]],没有中心元素。
  • n = 3,循环走一圈后最中间剩matrix[1][1],手动填9,整体螺旋顺序正确。
  • n = 4,两圈循环,每圈四条边全部左闭右开,最终矩阵完全符合预期。

我自己在第一次写这个题时犯过一个经典错误:把第一个for写成了j <= n - offset,导致右边界提前占位,第二圈的时候上下边错位,整个矩阵的螺旋顺序直接变形。后来把区间统一改成左闭右开,所有边界问题一次性消失。

4. 当天实测中遇到的三个坑

4.1 窗口收缩时机写错导致的错误更新

做209长度最小的子数组时,我最开始把ans的更新放在while循环外面,这样一旦sum >= target,我记录的是“右指针到达当前位置时的窗口长度”,但没有及时把左指针收缩后的最新长度记录下来。结果遇到类似[2,3,1,2,4,3]这种用例,窗口内明明能收缩到更短,我却因为更新时机太晚而输出错误答案。

正确的思路是:每收缩一次,就立刻记录当前[left, right]的长度,然后再继续收缩。换句话说,ans = min(ans, right - left + 1)必须放在while内部,而且最好放在收缩元素之前,因为此时窗口还是满足条件的。

4.2 对循环圈数n/2的误解

螺旋矩阵里,我一开始纠结为什么loop = n / 2而不是loop = n。后来想明白了:每一圈循环会处理掉矩阵最外面两行两列(上下左右四条边),所以n阶矩阵最多只需要n/2圈就能把所有外层剥完。奇数阶再单独补一个中心点,偶数阶则全部处理干净。

这个理解如果不到位,很容易把循环次数写多,导致数组越界,或者把已经填好的格子又覆盖一遍。所以我建议你拿到这题先手动画一个4×4和5×5的矩阵,标出每一圈的起始位置和结束位置,再动手写代码,效率高很多。

4.3 循环里j和i的作用域隐坑

第二个for里我在循环体外已经声明了int i = startX, j = startY;,因此四个for都用同一个j和i。第一次写的时候我习惯在for内重新声明int j = startY这样的局部变量,导致第一个循环结束后,新的局部j被销毁,第二个循环用的还是旧值,整个填充全乱套了。

这种问题只会在实际编译运行时暴露出来,光读代码很难察觉。我的经验是:在涉及多段连续边界操作的代码里,循环变量尽量在外层统一声明,让每段代码之间的状态传递是显式且可控的。

5. 一套可复用的刷题节奏与变式练习建议

5.1 我当天的时间分配

Day2的总复习时间我控制在三小时左右:

  • 前半小时复习数组基础、双指针套路,并重新过一遍Day1的二分模板。
  • 第一个小时专攻长度最小的子数组,先写暴力解,再推导滑动窗口优化。
  • 第二个小时专攻螺旋矩阵II,手动画出4阶和5阶矩阵的填数路径,然后写代码对比。
  • 最后半小时复盘两题的循环不变量与滑动窗口模板,整理错因,并把变式题的思路写进笔记。

这个节奏的好处是每一段都留有缓冲时间,不至于一卡住就整个放弃。如果你基础还不太稳,建议把前置复习时间适当拉长,但总时长尽量不要拖过4小时,否则学习状态容易疲。

5.2 值得趁热打铁的变式题

Day2这两题都有非常值得延伸的变式,我在当天一并总结了出来:

  • 长度最小的子数组的进阶版是LeetCode 76最小覆盖子串,它的思路本质上还是滑动窗口,但窗口条件从“和大于等于target”变成了“包含所有目标字符”,需要额外维护一个字符计数字典。
  • 螺旋矩阵的进阶版是LeetCode 54螺旋矩阵,区别在于矩阵不再是正方形,行数和列数可能不同,循环边界会更抽象一些,但循环不变量思想完全相通。
  • 如果还想训练前缀和思维,可以做LeetCode 560和为K的子数组,不过它用的是前缀和加哈希优化,这条线和209的滑动窗口略有区别,建议等彻底吃透窗口思想后再去碰。

5.3 给同样在刷Day2的朋友几句实在话

如果你已经跟着代码随想录刷到了Day2,说明你至少push自己迈出了第一步。这一阶段最不需要的就是和别人比进度。我在实际刷题过程中发现,很多人最后放弃并不是因为题目难,而是因为一卡住就自我怀疑,觉得是不是自己不适合写代码。其实真相是:滑动窗口和循环不变量这两个概念,本来就不是看一遍就能完全长在脑子里的,必须靠亲手debug几个用例、写错几次才能形成真正的感觉。

我自己一度在第3节那个for变量作用域的坑里卡了快半小时,最后通过加打印日志才看清楚问题。Debug到想砸键盘的时候,就把题目先放一放,去画个图、写个伪代码,等脑子里那根弦松了再回来,往往一下就通了。

这套Day2的记录就先写到这里,希望这篇能帮你少走一点弯路,也欢迎你在评论区留下你的做题心得或者你踩到的其他坑,我看到了会仔细回复。

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

麦肯锡2026技术趋势14_能源与可持续发展技术的未来_研究解读

从能源技术到可靠供给&#xff1a;麦肯锡 2026 年“能源与可持续发展技术的未来”研究解读 麦肯锡《Technology Trends Outlook 2026》技术趋势解读系列 第 14 篇 摘要 麦肯锡将“能源与可持续发展技术的未来”&#xff08;Future of energy and sustainability technologi…

作者头像 李华
网站建设 2026/10/11 7:16:03

一个接口调用十款文档解析模型:OpenDocRouter 拆解

一个接口调用十款文档解析模型&#xff1a;OpenDocRouter 拆解原文&#xff1a;LlamaIndex Blog - 《Introducing OpenDocRouter: every document model under one API》&#xff08;https://www.llamaindex.ai/blog/introducing-opendocrouter&#xff09;一、文档解析的难点&…

作者头像 李华
网站建设 2026/10/11 7:13:12

二分查找的本质与边界技巧:从有序数组到二分答案与浮点逼近

聊到二分查找&#xff0c;很多人的第一反应是“不就是在一个有序数组里找一个数嘛&#xff0c;写个 while (l < r) 就行”。但我在带新人、改算法的过程中发现&#xff0c;真正能把二分查找的应用玩明白的人真不多。它远远不止“查找”这么简单&#xff0c;更是一套“不断排…

作者头像 李华
网站建设 2026/10/11 7:13:03

云端与本地并行交付,软件许可管理如何实现统一运营?

摘要&#xff1a;同一款软件同时做云端、本地、内网、离线交付&#xff0c;授权规则最容易割裂成几套台账。这篇不讲概念&#xff0c;给一套可直接执行的验证方法&#xff1a;先解耦平台部署、运行环境、许可载体三个维度&#xff0c;再用"5 类环境 8 类生命周期事件&quo…

作者头像 李华
网站建设 2026/10/11 7:12:53

市场前景明朗:全球半导体键合设备预计2032年销售额突破64.94亿美元

2026年国内先进封装产能扩张进入深水区&#xff0c;大量晶圆制造与封测企业普遍面临半导体键合设备进口依赖度高、细间距异质集成工艺适配难、量产良率爬坡周期长的痛点&#xff0c;晶圆级混合键合、热压键合设备、Chiplet异质集成正成为破解行业痛点的核心方向。作为半导体先进…

作者头像 李华
网站建设 2026/10/11 7:10:30

构建AI助手技能系统:从架构设计到技能包开发实战

最近一直在折腾一件事&#xff1a;把常用的那个AI助手从“能聊几句”变成“真能干活”。核心就落在标题里那个词——skills。我给这套助手框架加了一层技能系统&#xff0c;让它不再只会生成文本&#xff0c;而是能去读文件、查数据、跑脚本&#xff0c;甚至定时执行任务。这篇…

作者头像 李华