数组进阶三题:滑动窗口、螺旋矩阵与前缀和实战解析 1. 选题逻辑第一天的数组热潮第二天就开始上强度了如果你的刷题计划是按专题推进的那第二天安排这三道题相当有讲究。209 是典型的滑动窗口应用题59 是模拟类题目的代表58 则直接拉开了前缀和的大幕。很多人第一天还在熟悉数组的基本遍历、二分查找第二天就会被这三道题卡住尤其是 59 这种“代码本身不复杂但就是绕不清楚”的题非常考验对循环边界的控制力。先说结论这三道题分别对应了数组专题中最常被面试官翻牌的三种能力——用双指针把O(n²)优化成O(n)、用循环不变量把手工模拟转成代码逻辑、用预处理把多次区间查询从O(n)降到O(1)。刷完这一组你对数组的理解会从“会遍历”升级到“会设计解法”这是质的区别。这篇文章会带着你从头到尾把三道题各写一遍不是说一句“这题用滑动窗口”就完事而是把为什么用、怎么写、哪里容易错全部铺开。我自己刷这组题的时候209 写了三版才彻底理顺窗口的收缩逻辑59 更是因为边界条件反复调试最后才发现是循环变量没对齐。希望你看完能少走这些弯路。2. 209. 长度最小的子数组暴力解没戏滑动窗口的思维转换2.1 先想清楚暴力为什么慢题目要求很直接给定一个正整数数组和一个目标值找出数组中满足“和 ≥ target”的连续子数组的最小长度如果不存在就返回 0。最直白的思路是枚举所有子数组的起点和终点两层循环把每个连续片段的和算一遍找到最短的那个。这个思路没有任何问题问题是数组长度如果到 10 的 5 次方级别O(n²) 的复杂度直接爆掉在大多数在线评测环境里跑不动。暴力慢在哪里慢在大量重复计算。你算了区间 [i, j] 的和紧接着算 [i, j1] 的和如果每次都是从 i 开始重新累加前面算过的所有元素都被浪费了。更关键的是很多长度已经很大、但和也满足条件的区间根本没必要继续往后扩展暴力解法不会做这种“提前刹车”。2.2 滑动窗口到底在滑什么滑动窗口的思路本质上还是双指针只不过让两个指针之间维护的是一个“连续区间”。右指针负责扩展窗口把新元素纳入进来当窗口内的和满足条件时记录当前窗口长度然后收缩左指针尝试找到一个更短的合法窗口。这个过程的精髓是窗口的右端只向右移动左端也只向右移动每个元素最多被“加入窗口”一次、“移出窗口”一次整体复杂度 O(n)。直接看代码C 写法如下class Solution { public: int minSubArrayLen(int target, vectorint nums) { int left 0; int sum 0; int result INT32_MAX; for (int right 0; right nums.size(); right) { sum nums[right]; // 窗口内元素和满足条件后尝试收缩左边界 while (sum target) { result min(result, right - left 1); sum - nums[left]; left; } } return result INT32_MAX ? 0 : result; } };这里最关键的是while (sum target)这个内层循环。很多人以为外层 for 一次遍历就结束了结果发现窗口收缩时还需要继续判断于是写成 if一收缩就不满足条件了答案直接错。记住只要当前窗口还满足“和 ≥ target”就要一直收缩下去直到不满足为止。举个小例子数组[2, 3, 1, 2, 4, 3]target 是 7。right 0sum 2不满足right 1sum 5不满足right 2sum 6不满足right 3sum 8满足记录长度 4收缩左边界left 走到 1sum 6不满足right 4sum 10满足记录长度 4收缩left 到 2sum 7仍满足记录长度 3继续收缩left 到 3sum 6不满足right 5sum 9满足记录长度 3收缩left 到 4sum 7仍满足记录长度 2继续收缩left 到 5sum 3结束。最终答案是 2也就是[4, 3]这个子数组。2.3 关键细节和常见误解整个滑动窗口最容易被忽略的是初始值的设定。result初始化为INT32_MAX而不是 0是因为如果从头到尾没有找到合法区间你无法区分“答案是 0”和“还没找到”。最后做一次判断如果result还是初始值就说明一个满足条件的子数组都没有。还有一点要提醒这个解法能成立的前提是数组里全是正整数所以窗口和随着右指针移动是单调不减的这也是为什么收缩到不满足条件就可以停。如果数组里存在负数窗口和就不是单调变化滑动窗口就不能直接用了得换前缀和 二分或者别的思路。这个前提条件面试时一定要能说出来能说明你是真的理解而不只是背了模板。3. 59. 螺旋矩阵II模拟类题目的生死线是循环不变量3.1 这道题到底难在哪给定一个正整数 n生成一个 n x n 的矩阵按顺时针螺旋顺序填入 1 到 n²。看起来是个纯手工活但动手写就发现全是坑每次转圈到底走几格、走到中心怎么处理、n 是奇数还是偶数会不会影响结果。很多第一次写的朋友会这样想用四个方向数组遇到边界就拐弯。这个思路不是不能做但判断“是否遇到边界”本身就很麻烦因为你每走一步都要检查下一个位置是不是已经填过数代码容易写得又长又乱。更清晰的思路是维护四条可收缩的边界。每次填完一行或一列对应的上、下、左、右边界就往中间收一格。循环一次就是一圈直到边界交错说明全部填完。3.2 左闭右开的循环不变量写法这里我推荐教科书写法中很经典的一套每一圈的四个边都用“左闭右开”的方式去填每条边只处理区间内的一部分元素避免边界条件重复也避免最后一个元素被填两次。class Solution { public: vectorvectorint generateMatrix(int n) { vectorvectorint matrix(n, vectorint(n, 0)); int top 0; int bottom n - 1; int left 0; int right n - 1; int num 1; while (top bottom left right) { // 从左到右填充上边 for (int col left; col right; col) { matrix[top][col] num; } top; // 从上到下填充右边 for (int row top; row bottom; row) { matrix[row][right] num; } right--; // 从右到左填充下边注意防止与上边重叠 if (top bottom) { for (int col right; col left; col--) { matrix[bottom][col] num; } bottom--; } // 从下到上填充左边注意防止与右边重叠 if (left right) { for (int row bottom; row top; row--) { matrix[row][left] num; } left; } } return matrix; } };这个写法中的四个边分别是[left, right]的闭区间填充因为矩阵本来就是一圈一圈收紧的使用闭区间反而更直观。如果你用左闭右开其实也一样能行只要边界的计算严格同步即可。我用这个版本讲是因为它对“每填完一条边就收缩边界”这件事表现得很直白适合作为理解题意的第一步。3.3 真正容易翻车的地方翻车点一般集中在单行或单列的场景。比如 n 1只有一个格子。此时 top 0bottom 0left 0right 0进入循环后第一次 for 填好matrix[0][0]然后top变成 1后续三个方向的循环都因为边界判断被跳过。这里如果没有if (top bottom)和if (left right)的保护就会在填充完上边后继续往下填右边把已经越界的区域写入导致数组越界或者重复赋值。还有一种常见写法是直接用方向数组“撞墙转向”那种做法要在每步判断下一个位置是否超出边界或者已经被填过数字逻辑上也能过但代码的可读性会差很多。我在实际刷题时两种都写过最后还是推荐边界收缩法因为它的循环不变量更清晰你只需要保证每执行完一个方向后对应的边界向中心收缩一格即可。4. 58. 区间和前缀和为什么能成为竞赛常客4.1 先看题目到底要算什么这道题和前面两道不太一样它不是一个标准的函数式题目而是更像 ACM 模式里的输入输出题。题干是说给定一个长度为 n 的整数数组然后有多次查询每次给一个区间[l, r]要求输出这个区间内所有元素的和。如果你直接对每次查询都写一个循环从 l 加到 r功能上没问题但效率很差。假设数组长度是 10 的 5 次方查询次数也是 10 的 5 次方最坏情况就是 10 的 10 次方次运算肯定超时。这也是这类题目普遍存在的意义——你必须想办法让每次查询不再是 O(n) 的扫一遍而是 O(1) 拿到结果。4.2 前缀和的本质是“预处理”前缀和的核心思想很简单用一个额外的数组pre其中pre[i]表示原数组从第 0 个元素到第 i 个元素的和。有了这个数组区间[l, r]的和就可以用一次减法得到sum(l, r) pre[r] - pre[l-1]这里的pre[l-1]是区间左端点前面那一个位置的前缀和。为了方便处理 l 0 的情况通常让pre的长度为 n 1并且pre[0] 0pre[i]表示前 i 个元素的和。这样区间[l, r]的和就直接是pre[r1] - pre[l]不需要特判。示例代码如下#include iostream #include vector using namespace std; int main() { int n; cin n; vectorint nums(n); for (int i 0; i n; i) { cin nums[i]; } // 构建前缀和数组pre[i] 表示前 i 个元素的和 vectorlong long pre(n 1, 0); for (int i 1; i n; i) { pre[i] pre[i - 1] nums[i - 1]; } int l, r; while (cin l r) { // 注意输入区间是左闭右闭且下标从 0 开始 cout pre[r 1] - pre[l] endl; } return 0; }4.3 输入输出模式和数据类型的选择58 这类题经常有多个查询而且输入的行数不定常见做法是持续读取直到文件结束所以用while (cin l r)。我第一次做类似的题时只读了一次查询就结束了结果后面的查询全部没有输出白跑一遍评测。另一个注意点是数据类型。很多初学者用int存前缀和当数组元素多、数值大的时候前缀和会超过 int 的表示范围造成溢出。稳妥的做法是用long long虽然题目给的示例可能很小但评测数据不会这么温柔。这里也顺便说一句前缀和适合的场景是数组是静态的也就是构建好之后不会频繁修改元素值。如果中间还要支持“改某个位置的值”的操作那就不是前缀和能解决的得考虑树状数组或者线段树了。这一点面试时经常会被追问提前想清楚这个边界条件会加分不少。5. 三题横向对比从数据结构的角度看它们的关系5.1 一道题一张图把三题放在一起看你会发现它们其实都在处理“区间”这个概念。209 的滑动窗口本质是在一个可变区间上维护窗口内元素和59 的螺旋矩阵虽然看起来是模拟填充但每一圈的填充也可以理解成四个边界构成的矩形区间在不断收缩58 的区间和则是直接对固定区间做查询。区别在于209 的窗口是动态伸缩的59 的边界是有规律地单向收缩58 的区间是静态的、多条查询。把这三道题放在同一天刚好能帮你建立一个全局视角同样是“区间”在不同约束下可以选择完全不同的算法。5.2 三种思考方式的适用场景题号/场景核心思路时间复杂度适用前提209滑动窗口双指针O(n)数组全为正数窗口和单调变化59边界收缩模拟O(n²)需要按规则生成矩阵无特殊前提58前缀和预处理预处理 O(n)查询 O(1)数组静态只查询不修改这张表是我后来复盘时自己总结的对你面试前的快速复习也有参考价值。很多时候面试官不是一上来就让你写代码而是先考你“给一个场景你用什么思路去解”能说出上面的表格内容基本就拿到了这道题的入场券。6. 实操过程中的问题排查技巧6.1 滑动窗口死循环的排查滑动窗口代码最典型的死循环场景是内层收缩逻辑写成了if或者收缩时指针移动的位置不对。比如有人把sum - nums[left]写成了两步先sum - nums[left]再left顺序没问题但如果在前面又判断了一次sum target中间变量没更新就可能出现窗口不收缩、结果无限循环或者结果错误。排查技巧是加日志每进入一次 while打印出当前的 left、right、sum、result很快就能看到窗口收缩到哪个环节出了问题。我在本地调试时一定会看这几个值。6.2 螺旋矩阵越界的定位螺旋矩阵的越界通常出现在单行或单列时原因是边界收缩后top、bottom、left、right的相对关系已经变化但后面的 for 循环没有加保护条件。解决方式就是我在 3.2 的代码里写的在每一条转向填充前判断边界是否还合法。还有一种情况是 n 为奇数时最中心那个格子会被最后一条边填充中间也会经过多轮边界收缩如果某一步把top和bottom交错后还在继续循环就会导致中心格子被跳过或者重复赋值。建议在 while 条件里同时检查两个维度top bottom left right不要只留一个判断。6.3 前缀和结果类型溢出的问题前缀和的求和结果可能超过 int但很少有人一开始就意识到。如果你发现某些测试用例计算结果异常大或者变成负数优先检查是否用了long long。还有一个小细节构建前缀和时如果用pre[i] pre[i - 1] nums[i - 1]注意nums[i-1]也可能会被隐式转换成更大的类型不要写反了数组下标。6.4 题号 58 的输入坑58 这类题目常常有两个输入坑。第一个是查询行数不确定需要用while (cin l r)持续读取第二个是数组下标到底是从 0 开始还是从 1 开始。不同题目可能不一样建议在写前缀和公式之前先用题目的示例数据手动推导一遍确定pre的下标对应关系再动手写代码。这比写完再调试快得多。我在实际做这类题时养成了一个习惯先在草稿纸上写出pre[i]的定义公式再找一个小样例手算一遍然后才打开编辑器。看似多了一步实际上省掉的调试时间远多于这一步的时间成本。7. 我的个人经验和后续学习建议这三题刷完我最大的感受是数组专题的真正分水岭不是“你会不会遍历”而是“你能不能根据题目条件判断该用哪种区间处理思路”。209 练的是动态窗口59 练的是控制边界58 练的是空间换时间。这三板斧在后面很多题里都会被复用。如果你今天刚开始做这一组题我的建议是不要追求一次 AC而是每道题先自己写一版暴力解再去优化。比如 209 先用 O(n²) 的版本跑一遍哪怕超时也没关系关键是通过对比体会“优化到底优化在哪一步”螺旋矩阵先试着用方向数组实现一遍哪怕代码啰嗦也能让你更深刻地体会边界收缩写法的价值区间和先写朴素查询再引入前缀和你会发现每次查询从“重新算一遍”变成“查表”这个对比比任何讲解都要具体。接下来可以继续往这些方向扩展滑动窗口可以延伸去刷 904 水果成篮、76 最小覆盖子串螺旋矩阵可以做 54 螺旋矩阵不填数而是读取数、48 旋转图像前缀和则可以配合二分做 974 和可被 K 整除的子数组这类进阶题目。基础打牢之后这些题上手会快很多。说到底这三道题不是终点而是一把尺子。量一量你对数组区间操作的理解到了什么程度同时也给了你一个清晰的补强方向。把这组题吃透后面的哈希表、双指针、前缀和进阶专题会顺畅得多。