如果你最近加入了一支刷算法题的队伍或者正自己按计划推进大概会认“代码随想录”这个名字。它在准备计算机面试的人群中流传很广特点是每个专题都按天拆好题目之间相互递进。day2 这个节点很典型第一天刚解决完二分查找和数组元素移除第二天马上进入三种很常见的数组处理方式——有序数组的平方、长度最小的子数组、螺旋矩阵 II。三道题放在同一天不是随手凑的。它们分别代表三种思维利用单调性的双指针、同向移动的滑动窗口、以及最考验边界控制的模拟循环。如果你刷完这组题后续再碰到“字符串子串”“矩阵旋转”“归并合并”这类问题会明显感觉有底。这篇文章就按我实际刷这组题时的顺序和踩坑记录来讲包含每道题的为什么这样解、完整可跑的代码、以及我反复出现的错误。适合跟着计划打卡的你也适合已经刷过但想查漏补缺的人。1. 先说清楚 Day 2 这三道题背后的共同逻辑1.1 为什么是“数组篇”的第二站代码随想录的打卡计划里数组篇的题目并不是按难度排列的而是按“动手方式”排列的。第一天先让你用二分查找完成“在一个有序数组里找一个数”再用双指针完成“原地移除指定元素”。这两道题解决的都是一个最基本问题怎么高效地操作数组中的元素位置。day2 把难度往上抬了一档有序数组的平方LeetCode 977考察的是如何利用数组已经有序这个前提长度最小的子数组LeetCode 209考察的是如何在连续区间上用双指针滑动螺旋矩阵 IILeetCode 59则是纯模拟不涉及复杂算法却非常考验对循环边界的把握。你如果只看题目数量会觉得一天三道题有点多。实际上它们的技术点是逐渐叠加的第一道从“两端往中间”移动指针第二道从“左往右”维持一个滑动区间第三道则把指针移动从一维扩展到二维矩阵。刷完以后你对“指针怎么动”“边界条件怎么定”会有一次很完整的肌肉记忆训练。这道题还有一个隐藏价值它训练你“把规则翻译成代码”的能力。很多业务需求虽然不涉及复杂算法但边界情况极多能在十分钟内写好 59说明你有能力在一个局部规则极其琐碎的场景里保持逻辑清晰。1.2 刷之前你应该具备的最小基础我见过不少新朋友直接跳到 day2结果被螺旋矩阵绕晕。所以先说清楚前置条件会写 for 循环和 while 循环能理解数组下标是从 0 开始的已经刷完 day1 的 704 二分查找和 27 移除元素至少对“左右指针”“区间不变量”有印象对“时间复杂度 O(n) 和 O(n²) 的差别”有模糊概念就够了不需要会严格推导。如果你满足这三条就可以继续往下看。螺旋矩阵那道题即使第一次没写出来也很正常它本来就是用来暴露你“边界感”短板的。2. 第一题有序数组的平方LeetCode 9772.1 题目到底在问什么题目给你一个按非递减顺序排序的整数数组 nums要求返回每个数字的平方组成的新数组且结果也必须按非递减顺序排序。示例中最经典的是这种输入 nums [-4, -1, 0, 3, 10] 输出 [0, 1, 9, 16, 100]最简单的思路是先平方再排序。代码三行用 Python 写就是def sorted_squares_brute(nums): result [x * x for x in nums] result.sort() return result这个写法在面试里不能说错但距离“最优解”有明显差距因为复杂度是 O(n log n)。而题目的关键信息“原数组已经有序”没有被用到。数组里负数平方后可能反超正数这个“先降后升”的形态是这道题的真正破题点。在面试中面试官通常不会满意你直接 sort。追问“能不能 O(n)”之后就需要双指针出场了。2.2 双指针为什么是标准答案对于一个已经有序的数组你可以想象它的平方序列特征两边最大中间最小。最大值只可能来自最左端或者最右端不可能来自中间。所以只要同时看数组两端把较大的平方值放到结果数组的末尾再从末尾向前填充就能一次性得到有序结果。思路清晰后实现就很直接def sortedSquares(nums): n len(nums) left, right 0, n - 1 result [0] * n pos n - 1 while left right: left_square nums[left] * nums[left] right_square nums[right] * nums[right] if left_square right_square: result[pos] left_square left 1 else: result[pos] right_square right - 1 pos - 1 return result这里有一个很多人第一次写都会犯的毛病比较的是绝对值吗其实直接比较平方值就行因为平方本身就是非负数。比较平方后较大的那个放入 result 的当前位置。为什么要用而不是因为当 left 和 right 指向同一个元素的时候这个元素也要被处理否则会漏掉中间那个数。换成后结果是错的整个数组会少一个元素而且那个元素往往就是平方后最小的那个。理解了这段代码你会发现它和“归并排序”里的合并步骤很像——两边各有一个有序序列每次取更大的那个。只不过这里不需要额外移动一个数组只是从同一个数组的两端向中间靠拢。2.3 我踩过的坑和给你划的重点这道题我最开始犯的错误有三个把结果数组从前往后填结果小的值先来最后覆盖成了乱七八糟的顺序while 条件写成了left right在数组长度是奇数时总会漏掉最中间的元素比较时想先判断正负结果写出了很多 if 分支搞得代码又长又容易错。正确的做法就是上面那样结果从后往前填、循环条件用、直接比较平方值。如果输入 nums 全部是负数或者全部是正数这个算法依然成立因为它永远在比较两端的平方大小和数组里有没有 0 没有关系。从复杂度看时间 O(n)空间 O(n)这是这道题的标准最优解。虽然 LeetCode 也接受暴力解法但能写出双指针说明你理解了“利用已知有序信息”这件事。3. 第二题长度最小的子数组LeetCode 2093.1 题目信息和暴力解法的天花板题目给一个正整数数组 nums 和一个正整数 target要求找满足“和 ≥ target”的长度最小的连续子数组返回长度如果不存在这样的子数组返回 0。最朴素的思路是枚举左端点再枚举右端点不断累加求和。这种做法在数组长度小的时候没问题但一旦长度到 10 万级别O(n²) 就会超时。LeetCode 的测试用例专门卡这种解法。我第一次做这道题时的想法是既然要找最短长度那就先固定一个左起点往右加数字一旦和超过 target就记下长度再换下一个左起点。这个思路方向是对的但它有个致命问题每次都从新的左起点开始重新累加前面的加法完全被浪费了。滑动窗口解决的就是这个浪费问题。3.2 滑动窗口到底怎么滑滑动窗口的本质是维护一个连续区间左边界 left右边界 right。right 负责往前探索扩大窗口当窗口内的和已经满足条件时left 开始往右收缩一边收缩一边记录当前窗口长度直到条件不再满足。然后 right 继续往前left 也继续跟着收缩。用代码写出来是这个样子def minSubArrayLen(target, nums): n len(nums) ans n 1 left 0 cur_sum 0 for right in range(n): cur_sum nums[right] while cur_sum target: ans min(ans, right - left 1) cur_sum - nums[left] left 1 return ans if ans n else 0很多人会问外层是 for内层还有个 while这时间复杂度还是 O(n) 吗是的。因为 right 每前进一次只把 nums[right] 加入一次left 每收缩一次只把 nums[left] 移出一次。每个元素最多被加入一次、移出一次所以总操作次数是 2n复杂度 O(n)。为什么收缩要用 while 而不是 if因为这正是最容易踩的坑。假设窗口现在已经满足条件你记录了一个长度。用 if 的话left 只收缩一次就继续让 right 往前跑了窗口依然可能满足条件但你漏掉了更短的窗口。拿 target 7nums [2, 3, 1, 2, 4, 3] 来说当 right 走到最后一个元素时窗口先变成 [2, 4, 3]和是 9长度 3继续收缩一次移掉 2窗口变成 [4, 3]和是 7长度 2。这个长度为 2 的答案只有用 while 连续收缩才能捕捉到。如果只收缩一次最终答案会停在 3直接错过了正确结果。所以窗口收缩是一个“能缩就缩到不能缩为止”的过程必须配合 while。这也是滑动窗口题目里最常见的细节考点。3.3 一个直接可用的滑动窗口模板从这道题里可以总结出一个模板很多包含“连续子数组”“子串”的问题都能套left 0 for right in range(n): 把 nums[right] 加入当前窗口状态 while 当前窗口满足题目条件: 更新答案通常是取最小/最大长度 把 nums[left] 移出当前窗口状态 left 1在 209 里窗口状态就是区间和在其他题里窗口状态可能是字符出现次数、去重集合、乘积等。核心不变右边界负责探索左边界负责在满足条件时收缩保证窗口一直满足“最短/最长”约束。这套模板适用的典型题目包括LeetCode 3 无重复字符的最长子串、LeetCode 76 最小覆盖子串、LeetCode 904 水果成篮。你把 209 吃透再遇到这些题时第一步就不再是懵的。写这道题时还有一个很容易被忽视的坑ans 的初始值。如果初始化成 0后面ans min(ans, len)永远是 0。建议初始化成n 1或者一个很大的数表示“当前还没找到合法窗口”。最后判断 ans 是否大于 n如果还是初始值说明不存在满足条件的子数组返回 0。4. 第三题螺旋矩阵 IILeetCode 594.1 这道题为什么让很多人崩溃螺旋矩阵 II 的题目极短给定正整数 n生成一个包含 1 到 n² 所有元素、且元素按顺时针螺旋排列的 n x n 矩阵。它没有复杂的算法不需要二分、不需要动归就是一个模拟“绕圈填数”的过程。但正因为它只考“边界控制”很多人反而容易翻车。我见过有人在面试里把这道题写了二十分钟还没调对最后只能换题。问题集中在哪四个方向填完一圈后边界要向内收缩但收缩到什么时候该停、奇数阶矩阵的中间格子谁来填、最后一个方向会不会把已经填过的格子覆盖……这些问题全都是边界条件。要写对这道题最稳的方式是定义四个边界变量left、right、top、bottom并用“左闭右闭”区间来理解每一段填充。4.2 用四个边界变量实现螺旋填充先看代码边界变量的含义都写在注解里def generateMatrix(n): matrix [[0] * n for _ in range(n)] left, right, top, bottom 0, n - 1, 0, n - 1 num 1 while left right and top bottom: # 从左往右填顶部一行 for col in range(left, right 1): matrix[top][col] num num 1 top 1 # 从上往下填右侧一列 for row in range(top, bottom 1): matrix[row][right] num num 1 right - 1 # 从右往左填底部一行 if top bottom: for col in range(right, left - 1, -1): matrix[bottom][col] num num 1 bottom - 1 # 从下往上填左侧一列 if left right: for row in range(bottom, top - 1, -1): matrix[row][left] num num 1 left 1 return matrix执行逻辑是这样的先填最外层一圈每填完一条边就把对应的边界往里收。比如填完 top 这一行top 加一填完 right 这一列right 减一。之后继续填剩下的内层矩形直到 left right 或 top bottom。为什么在填底部行和左侧列前要再加if判断这是防止重复填充的关键。在 n 为偶数时最后一轮矩形可能只剩一行或一列如果不检查底部的从右往左循环会把刚才从下往上填的格子再覆盖一次左侧列的循环也会重复。我们拿 n 3 来走一遍初始 left0right2top0bottom2第一轮填顶部一行得到 1、2、3top 变为 1填右侧一列从上到下得到 4、5right 变为 1从右往左填底部一行得到 6、7bottom 变为 1从下往上填左侧一列得到 8left 变为 1第二轮left1right1top1bottom1进入循环。从左往右给 matrix[1][1] 填 9top 变为 2。此时range(top, bottom1)即range(2, 2)为空right 变为 0。底部判断 top(2) bottom(1) 为假左侧判断 left(1) right(0) 也为假循环结束。所以 n3 时中间的 9 由第二轮的第一条水平填充完成。n2 时第一轮结束 leftright直接退出循环四周边界都被填好。在写了对奇偶两种情况都成立的边界判断后代码就是最通用的形态。4.3 面试现场怎么避免“写崩”我自己的经验是不管脑子里多清楚先别急着写代码。面试官允许甚至希望你画图。拿 n4 或 n3 画一个表格把 left、right、top、bottom 每轮的值写下来再开始写循环体。这样你写的不是逻辑而是“翻译”出错率会大幅下降。另外一个常见错误是循环不变量没想清楚。你用左闭右闭区间填顶部一行那右侧一列也必须用左闭右闭中间转弯的地方两条边会共享一个顶点。如果你某条边用了左闭右开、另一条用了闭区间最后要么少填一个要么重复填一个。统一的方式是四条边都用闭区间正序时用range(left, right 1)、range(top, bottom 1)倒序时用range(right, left - 1, -1)、range(bottom, top - 1, -1)。还有一个小技巧每次 for 循环执行完可以用print(matrix)看看中间矩阵长什么样。如果某个方向出现了覆盖通常就是边界判断没加或者顺序写反了。关于复杂度时间是 O(n²)因为你必须把 n² 个格子填满空间上除了结果矩阵外是 O(1)。5. 三种解题套路的横向对比和迁移方向5.1 一张表看懂三个套路我把三道题放在同一个坐标系里看它们其实是三种不同的“指针组织方式”题目核心思路指针移动方式时间复杂度最容易犯的错977 有序数组的平方双指针反向填充两端向中间O(n)while 条件用漏掉中间元素209 长度最小的子数组滑动窗口同向一前一后O(n)收缩时写成 if 而不是 while59 螺旋矩阵 II边界模拟四方向旋转O(n²)底部/左侧方向缺少边界判断这三者有一个共同的底层能力明确知道一个循环的开始和结束位置。977 里是 left 和 right 的相遇条件209 里是窗口左右边界的关系59 里是四个边界变量之间的包含关系。我把这些总结为“循环不变量”的实践版在 977 里不变量是“还没填进结果数组的元素一定在 left 和 right 之间”在 209 里不变量是“right 是当前窗口的右端点left 是满足条件前提下能达到的最右边界”在 59 里不变量是“每一轮填完四个边界向内收缩一格且当前待填位置不会落在已填区域”。面试时你把不变量说清楚比噼里啪啦写代码更能让面试官点头。5.2 后续刷题可以直接迁移的地方977 的双指针是“归并排序合并步骤”的极简版。数组归并需要两个指针分别指向两个有序序列每次取较小的加入结果977 则是一个序列内部用两个端点做伪归并。理解了它归并排序的 merge 函数你会写得更顺。209 的滑动窗口是“子串问题家族”的脚手架。我在刷 LeetCode 76 最小覆盖子串时直接把 209 的模板改成了字符计数版本主体循环几乎没动。LeetCode 3 无重复字符最长子串、LeetCode 424 替换后的最长重复字符也都是同一套思维。59 的边界控制在旋转矩阵、矩阵的螺旋遍历、以及二维数组的二分查找问题里都会用到。它们的共同点是你在二维空间里移动位置必须明确“下一步还在不在一行/一列的有效区间内”。6. 实测下来的复习节奏和避坑建议6.1 刷完 Day 2 后我建议你这样复习代码随想录的打卡通常是“今天刷、明天旧题复习、再往后推两题”。我对 day2 的建议是当天刷完三题后先别急着进 day3花二十分钟把三份代码重新读一遍重点看977 里 while 的结束条件为什么用209 里 while 收缩发生在 for 内部哪一个更新才真正改变了答案59 里底部和左侧两个方向的边界 if 为什么必不可少。很多时候你觉得“代码写出来了懂了”但一周后回头看才发现自己其实没记住边界条件的推导。刷算法题最怕的就是“眼睛会了、手不会”。我自己的做法是把三道题做成一个复盘表格字段包括题目编号、所用套路、指针移动规律、我错在哪一次提交、最小通过用例。LeetCode 会记录提交失败我会对比“失败版本”和“通过版本”的差异把差异原因写进表格。这种方法对我判断自己是否真的理解题目很有用。6.2 常见报错的现场排查流程如果你照着上面的代码写还是报错可以按顺序排查977 结果顺序不对看你是不是在结果数组开头插入元素了。如果结果是从前填充那么填入顺序应该是从小到大但双指针算出的是从大到小两者矛盾正确的做法是从后往前填。977 漏元素检查循环条件。left right会漏掉 left right 的那次赋值。209 返回 0 但明明有解多半是 ans 初始化成了 0后续 min 永远取不到合理长度。初始化成 n1 或 float(inf)。209 运行超时检查内部收缩是不是 while如果是 if极端用例会被迫让外层的 right 重复扫描复杂度退化。59 结果某一行全被覆盖八成是底部/左侧方向少了边界判断把上一轮已填的格子又填了一遍。59 中间数字没填奇数 n 的中间格可能在第二轮被跳过。仔细检查你的 while 条件是left right and top bottom而不是。这几个问题我在 LeetCode 评论区里经常看到自己也都踩过。尤其是“中间数字没填”和“只用 if 收缩窗口”这两个属于一看就是“以为懂了但没懂”的典型症状。6.3 个人经验这一天的内容值得二刷如果只允许我给一句“过来人”建议我会说day2 的三道题最值得二刷的不是最难的螺旋矩阵而是貌似简单的 209。因为 977 和 59 只要记住边界条件就能写对而 209 的滑动窗口思想会反复出现在后面几十道题里值得你用不同的变形题反复加深印象。我当时二刷 209 时故意不看模板只用自然语言描述逻辑“右指针前进窗口大了左指针收缩窗口小了一旦满足条件就记录长度。”写成代码跟模板一比果然一模一样。这说明套路已经内化了。另外一个小提示代码随想录里的题解用 C 居多但你不用被语言限制。无论是 Python、Java、Go核心都是同一个“循环不变量”思路。我在上面给出的 Python 版本足够跑通 LeetCode换成你熟悉的语言时只要保持边界判断一致即可。这就是我刷完代码随想录 day2 后的完整记录。希望这篇能帮你把三道题一次性吃透不用再像我一样踩完坑才回头补补丁。