螺旋矩阵
不背方向口诀;维护一个不断缩小的未访问矩形。
一句话理解问题
把矩阵看成一个会不断缩小的未访问矩形:每轮依次读完上、右、下、左四条边,再把对应边界向内收一格。
3×4 矩阵 → 12 项螺旋序列每个格子都必须进入 ans,而且只能进入一次。
不是旋转矩阵,也不是格子寻路矩阵位置始终不动;我们只改变读取顺序和剩余边界。
核心结论
top..bottom × left..right边界内恰好是尚未输出的矩形,边界外全部已经完成。
append edge → move boundary只删除刚刚完整读完的一条边,因此不会越过未访问格。
top<=bottom / left<=right前两条边收缩后,下边或左边可能已经不存在。
动画实验台
36 帧全部可以手动前后步进。每个数字各占一个 append 帧,判断、读取和边界写回绝不合并。重点观察 Step 31–33: 一个门禁失败,另一个门禁成立但内部循环仍执行 0 次。
算法推导:为什么是四条边界
如果用当前位置加方向来模拟,就要额外判断下一格是否越界、是否访问过, 转弯后还要继续维护 visited。螺旋遍历的已访问区域非常规则: 它总是从外到内完整消失一条边,因此可以直接描述“剩下什么”。
用 top、bottom、left、right 围出尚未读取的矩形。
当前边还在边界内,所以其中每格都是尚未输出的合法目标。
边界向内移动后,刚读过的格永久落到剩余矩形之外。
动画拆解:判断、读取、写回必须分开
top <= bottom只回答边是否存在,不读取格子,也不提前收缩边界。
ans = append(ans, matrix[row][col])每帧只追加一个确定坐标的值,ans 长度加一。
top++ / right-- / bottom-- / left++整条边读完后,才把它从未访问矩形中删除。
row=2 <= bottom=1进入某个分支也不代表内部 for 必执行,另一个维度仍会检查。
Go 代码
上边和右边在每轮入口处必然存在,可以直接扫描。它们收缩后, 下边或左边可能消失,所以后两条边各有一个防重复门禁。
1func spiralOrder(matrix [][]int) []int {2if len(matrix) == 0 || len(matrix[0]) == 0 {3return []int{}4}5rows, cols := len(matrix), len(matrix[0])6top, bottom := 0, rows-17left, right := 0, cols-18ans := make([]int, 0, rows*cols)9for top <= bottom && left <= right {10for col := left; col <= right; col++ {11ans = append(ans, matrix[top][col])12}13top++14for row := top; row <= bottom; row++ {15ans = append(ans, matrix[row][right])16}17right--18if top <= bottom {19for col := right; col >= left; col-- {20ans = append(ans, matrix[bottom][col])21}22bottom--23}24if left <= right {25for row := bottom; row >= top; row-- {26ans = append(ans, matrix[row][left])27}28left++29}30}31return ans32}
代码执行过程
| 阶段 | 扫描坐标 | 追加 | 边界写回 | ans |
|---|---|---|---|---|
| 外圈上边 | (0,0) → (0,3) | 1,2,3,4 | top: 0 → 1 | 4 |
| 外圈右边 | (1,3) → (2,3) | 8,12 | right: 3 → 2 | 6 |
| 外圈下边 | (2,2) → (2,0) | 11,10,9 | bottom: 2 → 1 | 9 |
| 外圈左边 | (1,0) | 5 | left: 0 → 1 | 10 |
| 内圈上边 | (1,1) → (1,2) | 6,7 | top: 1 → 2 | 12 |
不检查 top <= bottom上边收缩后,上下边界可能已经交叉。
不检查 left <= right右边收缩后,左右边界可能已经交叉。
复杂度
每个格子恰好 append 一次,所有循环总次数等于矩阵格数。
不计算返回数组,只维护四条边界与 row、col。
ans 必须容纳题目要求返回的全部元素,通常不计入额外空间。
面试表达
“我用 top、bottom、left、right 表示尚未访问的矩形。每轮按上、右、下、左 扫描四条边,扫完一条边再收紧对应边界。因为上边和右边收缩后可能只剩单行 或单列,所以扫下边前检查 top<=bottom,扫左边前检查 left<=right,防止重复访问。外层条件表示剩余矩形仍存在。 每个格子恰好访问一次,时间 O(mn),除输出外空间 O(1)。”
复盘总结
它们围出的区域恰好是尚未输出的矩形,不是当前指针位置。
扫描前这条边仍属于未访问区域;完整输出后才有资格删除。
top<=bottom 防单行重复,left<=right 防单列重复。
任一维度的边界交叉,未访问矩形不存在,外层循环终止。