跟着代码随想录刷题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, vectorint 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: vectorvectorint generateMatrix(int n) { vectorvectorint matrix(n, vectorint(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的记录就先写到这里希望这篇能帮你少走一点弯路也欢迎你在评论区留下你的做题心得或者你踩到的其他坑我看到了会仔细回复。