当前:LC54 · 螺旋矩阵 · 首次出现于 Day 53 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC54MediumMatrix · Simulation

螺旋矩阵

不背方向口诀;维护一个不断缩小的未访问矩形。

输入
3×4 matrix · 12 cells
输出
[1,2,3,4,8,12,11,10,9,5,6,7]
核心抓手
扫一条边 → 收紧这一边
01

一句话理解问题

把矩阵看成一个会不断缩小的未访问矩形:每轮依次读完上、右、下、左四条边,再把对应边界向内收一格。

题目要求
3×4 矩阵 → 12 项螺旋序列

每个格子都必须进入 ans,而且只能进入一次。

不是这些问题
不是旋转矩阵,也不是格子寻路

矩阵位置始终不动;我们只改变读取顺序和剩余边界。

02

核心结论

为什么四个整数就能替代 visited 表,并保证既不漏格也不重复?
状态不变量
top..bottom × left..right

边界内恰好是尚未输出的矩形,边界外全部已经完成。

先扫描再收缩
append edge → move boundary

只删除刚刚完整读完的一条边,因此不会越过未访问格。

防重门禁
top<=bottom / left<=right

前两条边收缩后,下边或左边可能已经不存在。

记忆重点不是“右下左上”,而是:当前还剩哪个矩形,刚刚读完哪条边, 收缩后下一条边还存在吗?
03

动画实验台

36 帧全部可以手动前后步进。每个数字各占一个 append 帧,判断、读取和边界写回绝不合并。重点观察 Step 31–33: 一个门禁失败,另一个门禁成立但内部循环仍执行 0 次。

Step 1/363%
主视觉 · 未访问矩形
边界内尚未输出,边界外已经完成
top
bottom
left
right
1(0,0)
2(0,1)
3(0,2)
4(0,3)
5(1,0)
6(1,1)
7(1,2)
8(1,3)
9(2,0)
10(2,1)
11(2,2)
12(2,3)
本帧判断
输入 12 格 → 输出 12 项
状态不变量
四边界内恰好是尚未输出的矩形
变量变化:尚未初始化边界
输出轨道 ans
len = 0/12
·
·
·
·
·
·
·
·
·
·
·
·
04

算法推导:为什么是四条边界

如果用当前位置加方向来模拟,就要额外判断下一格是否越界、是否访问过, 转弯后还要继续维护 visited。螺旋遍历的已访问区域非常规则: 它总是从外到内完整消失一条边,因此可以直接描述“剩下什么”。

1
描述剩余区域

用 top、bottom、left、right 围出尚未读取的矩形。

2
读取一条完整边

当前边还在边界内,所以其中每格都是尚未输出的合法目标。

3
删除已完成边

边界向内移动后,刚读过的格永久落到剩余矩形之外。

外层条件 `top <= bottom && left <= right` 表示两个维度都还有合法范围;它判断的是矩形是否存在。
05

动画拆解:判断、读取、写回必须分开

判断
top <= bottom

只回答边是否存在,不读取格子,也不提前收缩边界。

读取
ans = append(ans, matrix[row][col])

每帧只追加一个确定坐标的值,ans 长度加一。

写回
top++ / right-- / bottom-- / left++

整条边读完后,才把它从未访问矩形中删除。

空循环
row=2 <= bottom=1

进入某个分支也不代表内部 for 必执行,另一个维度仍会检查。

Step 12 已 append 4,但 top 仍为 0;Step 13 才执行 top++。 这正是代码真实执行顺序。
06

Go 代码

上边和右边在每轮入口处必然存在,可以直接扫描。它们收缩后, 下边或左边可能消失,所以后两条边各有一个防重复门禁。

Go · O(m·n) / O(1)
1func spiralOrder(matrix [][]int) []int {
2 if len(matrix) == 0 || len(matrix[0]) == 0 {
3 return []int{}
4 }
5 rows, cols := len(matrix), len(matrix[0])
6 top, bottom := 0, rows-1
7 left, right := 0, cols-1
8 ans := make([]int, 0, rows*cols)
9 for top <= bottom && left <= right {
10 for col := left; col <= right; col++ {
11 ans = append(ans, matrix[top][col])
12 }
13 top++
14 for row := top; row <= bottom; row++ {
15 ans = append(ans, matrix[row][right])
16 }
17 right--
18 if top <= bottom {
19 for col := right; col >= left; col-- {
20 ans = append(ans, matrix[bottom][col])
21 }
22 bottom--
23 }
24 if left <= right {
25 for row := bottom; row >= top; row-- {
26 ans = append(ans, matrix[row][left])
27 }
28 left++
29 }
30 }
31 return ans
32}
07

代码执行过程

阶段扫描坐标追加边界写回ans
外圈上边(0,0) → (0,3)1,2,3,4top: 0 → 14
外圈右边(1,3) → (2,3)8,12right: 3 → 26
外圈下边(2,2) → (2,0)11,10,9bottom: 2 → 19
外圈左边(1,0)5left: 0 → 110
内圈上边(1,1) → (1,2)6,7top: 1 → 212
不检查 top <= bottom
单行会被正向、反向各读一次

上边收缩后,上下边界可能已经交叉。

不检查 left <= right
单列会被向下、向上重复读取

右边收缩后,左右边界可能已经交叉。

08

复杂度

时间复杂度
O(m·n)

每个格子恰好 append 一次,所有循环总次数等于矩阵格数。

额外空间
O(1)

不计算返回数组,只维护四条边界与 row、col。

输出空间
O(m·n)

ans 必须容纳题目要求返回的全部元素,通常不计入额外空间。

09

面试表达

我用 top、bottom、left、right 表示尚未访问的矩形。每轮按上、右、下、左 扫描四条边,扫完一条边再收紧对应边界。因为上边和右边收缩后可能只剩单行 或单列,所以扫下边前检查 top<=bottom,扫左边前检查 left<=right,防止重复访问。外层条件表示剩余矩形仍存在。 每个格子恰好访问一次,时间 O(mn),除输出外空间 O(1)。
10

复盘总结

四条边界的完整语义是什么?

它们围出的区域恰好是尚未输出的矩形,不是当前指针位置。

为什么扫描完才移动边界?

扫描前这条边仍属于未访问区域;完整输出后才有资格删除。

两个 if 分别防什么?

top<=bottom 防单行重复,left<=right 防单列重复。

什么时候结束?

任一维度的边界交叉,未访问矩形不存在,外层循环终止。

最终记忆句:读完哪条边,就收紧哪条边;读下边和左边之前, 先确认它们没有在前两次收缩中消失。