news 2026/10/15 4:37:37

螺旋矩阵II精解:边界收缩与方向数组实现对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
螺旋矩阵II精解:边界收缩与方向数组实现对比

1. 题目拆解:螺旋矩阵到底在考什么

先把这个题目说清楚。LC 59.螺旋矩阵 II,输入一个正整数 n,要求生成一个 n×n 的矩阵,矩阵里的元素按照 1 到 n² 的顺序,以顺时针螺旋的方式依次填入。比如 n=3,输出就是:

1 2 3 8 9 4 7 6 5

我当年第一次做这道题的时候,脑子里想的是:不就是转圈填数字吗?外层一圈填完填内层,内层一圈再填满,用几个循环控制一下坐标就行。结果真正动手写,边界条件处理得乱七八糟,不是数组越界就是数字覆盖,调了一个小时才跑通。后来在面试模拟中又遇到几次,才彻底把这类题吃透。

这道题看起来简单,但它背后考察的东西其实不少。首先是对二维数组坐标的敏感度:行和列哪个是 i 哪个是 j,方向转换时坐标怎么变化,这是基础中的基础。其次是循环不变量思维:每一层填充的区间是左闭右开还是左闭右开、边界在什么时候收缩,这些不弄清楚,写出来的代码就是一团浆糊。再就是代码的组织能力:是写四个独立的 for 循环,还是用方向数组统一处理,不同写法体现的工程思维完全不一样。

从 LeetCode 的题目统计来看,这道题的通过率在 60% 左右,属于典型的中等偏易题目。但它在大厂笔试和面试中出现频率不低,尤其是考察候选人有没有写过图形类算法、能不能快速理清边界逻辑的时候,这道题是个很好用的筛子。

顺便说一句,LC 54.螺旋矩阵是它的姊妹题,一个负责生成、一个负责遍历,两道题放在一起刷效果更好。后面我会专门讲到它们的关联与扩展。

2. 核心思路:四种方向状态的切换

2.1 状态机思维

螺旋填充的本质是一个状态机。当前在某个坐标上,面向某个方向(右、下、左、上),如果下一个位置还在边界内并且没有被填充过,就继续前进;如果越界或者撞上了已经填过的位置,就切换方向。

这个思路用文字说很简单,但落到代码上有两种主流写法:一种是维护四个边界变量(top、bottom、left、right),每填完一条边就收缩对应的边界;另一种是维护一个方向数组(dx、dy),配合 visited 标记数组来判重。两种写法各有优劣,我在下面的章节里会分别给出完整的代码实现。

先说一个容易让人迷惑的点:为什么需要边界收缩,而不是单纯用“下一步是否越界”来判断?因为螺旋填充的轨迹是层层向内收的,第一圈结束后,第二圈的起点在 (1,1),它的上边界和左边界都变了。如果只用坐标范围去判断,你会在转向的瞬间搞不清当前到底应该按照哪一层边界走,所以要动态维护当前的“活动区域”。

2.2 复杂度分析

无论用哪种写法,时间复杂度都是 O(n²),因为矩阵里一共有 n² 个格子,每个格子恰好被填充一次。空间复杂度方面,四边界写法只需要 O(1) 额外空间(存四个边界变量),方向数组写法需要 O(n²) 的空间存 visited 标记数组,但如果直接在原矩阵里用“是否已被赋值”来判断,也可以把额外空间压回 O(1)。

这里有个面试加分细节:如果能主动说出“可以用当前矩阵值是否为默认值来判断该格子有没有填过”,而不是新开一个 visited 数组,说明你对空间复杂度有意识。面试官通常会因此多给你一点时间展开后续题目。

3. 解法一:四边界收缩法(推荐优先掌握)

3.1 核心思想

四边界收缩法的思路非常直观。初始时刻,矩阵的空闲区域是:

top = 0 bottom = n - 1 left = 0 right = n - 1

然后开始循环:

  1. 从左往右填充 top 行,填充完毕后 top++。
  2. 从上往下填充 right 列,填充完毕后 right--。
  3. 从右往左填充 bottom 行,填充完毕后 bottom--。
  4. 从下往上填充 left 列,填充完毕后 left++。

这四个步骤形成一个闭环,外层一圈填完后,top、bottom、left、right 各自向内收缩一格,天然形成了一个更小的矩形区域。循环继续,直到填充的数字超过 n² 为止。

这个方法的优点在于:边界状态都被显式地记录在四个变量里,每一圈的填充范围一目了然,代码逻辑非常清晰,基本不会出现“方向切了一半不知道自己在哪”的问题。

3.2 代码实现

以 C++ 为例,完整代码如下:

class Solution { public: vector<vector<int>> generateMatrix(int n) { vector<vector<int>> matrix(n, vector<int>(n, 0)); int top = 0, bottom = n - 1; int left = 0, right = n - 1; int num = 1; int maxNum = n * n; while (num <= maxNum) { // 1. 从左到右填充 top 行 for (int j = left; j <= right && num <= maxNum; ++j) { matrix[top][j] = num++; } ++top; // 2. 从上到下填充 right 列 for (int i = top; i <= bottom && num <= maxNum; ++i) { matrix[i][right] = num++; } --right; // 3. 从右到左填充 bottom 行 for (int j = right; j >= left && num <= maxNum; --j) { matrix[bottom][j] = num++; } --bottom; // 4. 从下到上填充 left 列 for (int i = bottom; i >= top && num <= maxNum; --i) { matrix[i][left] = num++; } ++left; } return matrix; } };

我在这个版本里加了num <= maxNum这个判断,目的就是防止当 n 为偶数时,最后一轮循环四个方向都执行一遍,把已经填过的格子覆盖掉。这个细节非常关键,我第一次写的时候没加,n=4 的时候直接输出错乱。

3.3 边界细节:循环不变量

写这类题目时最重要的一个概念是循环不变量,也就是“每次循环遵循的规则保持一致”。在上面的代码里,循环不变量是:每次填充当前边界所代表的行或列时,区间都是闭区间,即 [left, right] 和 [top, bottom],四个角分别属于两条边的交点,保证没有缺口也没有重叠。

你可能注意到了一个细节:++top放在for循环之后,这意味着第 1 行填完后,下一次“从下往上”填充 left 列时,起始行是新的 top,而不是旧的 top。这样就不会把刚才填过的那一行末尾的格子再填一次。每个方向的边界收缩时机都严格放在对应方向填充结束之后,这是避免元素覆盖的第一原则。

4. 解法二:方向数组 + 转向法(工程思维更强)

4.1 核心思想

第二种思路是把问题抽象成“机器人走路”:定义一个方向数组,按右、下、左、上的顺序排列。每走一步,先判断下一个位置是否越界或者已填充,如果合法就走过去并填入数字,如果不合法就原地转向,直到走完 n² 步。

这种做法的好处是:代码只维护一个“当前位置”和一个“当前方向”,不需要同时追踪四个边界。规则统一,扩展性强。比如改成逆时针螺旋、改成从外圈向内但起始方向不同,只需调整方向数组的顺序即可,逻辑主体完全不用动。

4.2 代码实现

用 Python 写出来的版本非常干净:

class Solution: def generateMatrix(self, n: int) -> List[List[int]]: matrix = [[0] * n for _ in range(n)] # 方向顺序:右、下、左、上 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] cur_dir = 0 x, y = 0, 0 for num in range(1, n * n + 1): matrix[x][y] = num # 标记当前位置已访问 # 这里利用矩阵中默认值 0 来标记,访问后变成非 0 值 next_x = x + directions[cur_dir][0] next_y = y + directions[cur_dir][1] # 如果下一个位置越界或者已经被填充,转向 if (next_x < 0 or next_x >= n or next_y < 0 or next_y >= n or matrix[next_x][next_y] != 0): cur_dir = (cur_dir + 1) % 4 next_x = x + directions[cur_dir][0] next_y = y + directions[cur_dir][1] x, y = next_x, next_y return matrix

这段代码的核心思想是“提前判断一步”:在填充当前格子之后,立刻预计算下一个位置,如果下一个位置不可达,就原地转向,再计算一次。这样每一步都在更新位置,循环里不需要显式的边界变量。

这里有一个非常关键的细节:判断“已填充”的条件是matrix[next_x][next_y] != 0。因为矩阵初始化时全部为 0,而填入的数字从 1 开始,所以只要不是 0 就说明这个格子已经走过了。这种方法省去了额外开一个 visited 布尔数组的开销,是空间复杂度上的一个优化技巧。

4.3 两种解法对比

维度四边界收缩法方向数组转向法
可读性逻辑直观,适合初学者状态清晰,适合工程化扩展
空间复杂度O(1),不需要额外标记可 O(1)(利用原矩阵值判断),一般 O(n²)
边界控制需要小心处理四个方向的收缩时机只需关注下一个位置的合法性
扩展性改动逻辑较多方向数组一换,就能改成逆时针等变体
调试难度比较容易出索引问题容易出现“走一步忘一步”的坐标错位

我在实际面试中通常先讲四边界法,因为它的每一步都能跟面试官同步清楚;等面试官继续追问“还有没有别的思路”时,再补充方向数组法。这样既显得基本功扎实,又能展示思维的灵活性。

5. 实操过程中的典型 bug 与调试技巧

5.1 经典 Bug 速查

这道题的 bug 模式非常集中,几乎所有人踩的坑都是同一批。我整理了一个速查表,方便你排查的时候对照:

症状可能的根因修复方式
矩阵里有格子是 0,没被填到while 循环条件判断有误,提前退出用num <= maxNum而不是num < maxNum
某些数字被覆盖了,矩阵里有重复数字收缩边界的时机不对,或者闭区间写成了开区间检查四个++top、--right等是否在对应 for 循环之后
n=2 时输出错乱偶数 n 的最后一圈被重复走,方向数组/边界收缩没处理好在每轮循环开头/结尾增加“如果 num 已超过 maxNum 则退出”的判断
越界异常方向数组的下一位置没有做合法性判断每次用next_x, next_y前先检查是否在 [0, n-1] 范围内
螺旋方向反了(逆时针)方向数组顺序不对把 directions 改成(0,1), (-1,0), (0,-1), (1,0)即可

5.2 调试技巧:打印矩阵

我调试这类题目时,最常用的方法就是写一个打印二维数组的辅助函数,每填充完一条边就调一次。举个例子,n=4 的时候,你可以看到每轮循环的边界变化:

Round 1: 填充上边: 1 2 3 4 填充右边: 5 6 7 8 填充下边: 9 10 11 12 填充左边: 13 14 15 16 当前边界: top=1, bottom=2, left=1, right=2

如果中间某个数字的位置不对,一眼就能定位是哪条边出了问题。比看一堆 console.log 或断点调试效率高得多。

5.3 面试中的检查顺序

在面试现场,写完代码后我建议按照以下的检查顺序过一遍:

  1. 先用 n=1 走一遍,确认矩阵只有一个元素,输出[[1]]。
  2. 再用 n=3 走一遍,这个用例能覆盖“有中心点”的情况,检查中心数字是否是 9,且最后一步转向正常。
  3. 最后用 n=4 走一遍,重点检查偶数 n 时会不会出现多绕半圈或覆盖的问题。

这三个用例都跑通了,这道题基本就没问题了。

6. 扩展变体:从生成到遍历,举一反三

6.1 逆时针螺旋填充

如果题目改成逆时针填充,用四边界法需要同时修改四个 for 循环的方向,比较麻烦;但用方向数组法,只需要把directions换成(0,1), (1,0), (0,-1), (-1,0)的逆序版本即可。这说明方向数组法的扩展性优势在变体题中特别明显。

6.2 矩形矩阵螺旋填充(LC 54 的前半部分)

如果输入不是 n×n,而是 m×n 的矩阵,需要先遍历输出所有元素,而不是填满整个矩阵。这时候四边界法依然适用,核心改动是:不能直接用num <= maxNum来判断,而是要维护一个count变量记录已经输出的元素个数,当count == m * n时退出。

6.3 从中心向外螺旋填充

这个变体稍微绕一些。不是先填外面的圈,而是从中心元素开始,按螺旋方式向外扩散。通常解法是先确定中心坐标,然后逐步扩大半径,或者把方向数组法与距离判定结合起来。它的核心难点不再是边界收缩,而是如何判断当前层应该走多长。

6.4 蛇形填充

蛇形填充和螺旋填充不是一回事。蛇形填充是:第 1 行从左到右,第 2 行从右到左,第 3 行再从左到右,如此反复。它的代码实现比螺旋简单很多,但面试官有时候会故意把两个概念放在一起问,考察你能否准确区分的边界条件。

我建议你刷完 LC 59 后,紧接着做三件事:把 LC 54 做一遍、把 n 改成矩形(m × n)再写一遍、把方向数组换成逆时针再写一遍。这三步做完,螺旋类题目你基本不会再有死角。

7. 实战经验总结

从我个人的经验来看,LC 59 这道题形似简单,但它适合用来训练一个很重要的编程习惯:先把“状态变量”梳理清楚,再动手写代码。螺旋矩阵的状态变量无非是四个边界或方向数组,但很多人一上来就写 for 循环,写到一半发现变量之间的关系理不清,然后开始到处打补丁,最后代码虽然跑通了,自己也讲不清每一步的逻辑。

我建议你刷题时按这个顺序来:

  1. 先自己默写一遍四边界法,不要看任何参考代码。
  2. 再用方向数组法重写一遍,感受两种写法的思考模式差异。
  3. 把两种写法的边界条件都画出图来,用箭头标出每一步坐标的变化。
  4. 最后合上代码,只对着一个 4×4 的空白格子,用手推一遍数字填充的顺序。

如果你能做到第 4 步,说明你对这个题目的理解已经是面试官最想看到的状态:不是背代码,而是真的掌握了坐标变化的规律。这道题后续无论怎么变,你都能快速定位到它的核心状态机上。

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

医院预约挂号系统毕设实战:Spring Boot + 小程序从开发到答辩

每年一到毕设季&#xff0c;"医院预约挂号系统"这种题目就会准时出现在各种选题清单里。今年我又接到了好几个朋友的咨询&#xff0c;都是同一句话&#xff1a;学长&#xff0c;这个基于 Spring Boot 和小程序的医院预约挂号系统到底怎么做才能顺利过审&#xff1f;说…

作者头像 李华
网站建设 2026/10/15 4:37:01

无数字字母RCE深度拆解:从PHP异或取反到安全防御实战

在CTF圈子里&#xff0c;"无数字字母RCE"几乎是每个做过Web题的人都会撞上的经典课题。我第一次看到这类题目名的时候&#xff0c;内心是拒绝的&#xff1a;eval把所有数字和字母全过滤了&#xff0c;剩下$()[];~^!这一堆特殊符号&#xff0c;还想执行命令&#xff1…

作者头像 李华
网站建设 2026/10/15 4:36:46

转行网络安全缺项目经验?三个实战项目带你写出第一份安全简历

我见过不少准备转行网络安全的朋友&#xff0c;刷完了一堆课程&#xff0c;装了各种工具&#xff0c;可一到写简历的时候&#xff0c;项目经历那一栏还是空的。这不是人不努力&#xff0c;而是大部分新手学习路径里缺了一个关键环节&#xff1a;能拿得出手、讲得清楚、经得起追…

作者头像 李华
网站建设 2026/10/15 4:36:10

Simulink MPSK仿真误码率异常排查:参数设置与调试指南

简介&#xff1a;面向通信工程专业学生与Simulink初学者的MPSK系统仿真实验资料&#xff0c;围绕多进制数字相位调制的基本原理、调制解调电路搭建与性能对比展开&#xff0c;可用于课程设计、实验报告或自学入门。文档详细介绍了MPSK信号的正交调制表达、相位配置矢量图、频带…

作者头像 李华
网站建设 2026/10/15 4:36:10

Mac硬盘扩容后如何彻底重建Git与Node开发环境

换硬盘、做迁移这种事情&#xff0c;真是每年都要来一次。尤其当你的 Mac 空间吃紧&#xff0c;扩完容把数据导回来&#xff0c;满心欢喜地打开终端准备继续写代码&#xff0c;结果发现环境一团糟&#xff1a;git还能提交但 SSH 连不上私有仓库&#xff0c;node能跑但npm装包报…

作者头像 李华
网站建设 2026/10/15 4:35:33

BOM转化管理成熟度测评表:从EBOM到MBOM的数据治理与改进实践

很多搞制造业的朋友都遇到过这个场景&#xff1a;公司花了几千万上ERP、上PLM、上MES&#xff0c;结果一盘点&#xff0c;物料齐套率长期在70%上下徘徊&#xff0c;变更单满天飞&#xff0c;车间里天天开会救火。查来查去&#xff0c;最后所有线索都指向同一个地方——BOM转化。…

作者头像 李华