螺旋矩阵:四边界一圈圈剥洋葱
用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。
用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。
当前方向、四条边界和剩余未处理区域分别是什么?
每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
先说结论:这道题到底解决什么
怎样从“用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。”推导出 矩阵模拟 · 四边界收缩,并证明每次状态变化都不会漏掉答案?
中心结论:每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
- 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
- 2.当前方向、四条边界和剩余未处理区域分别是什么?
- 3.不变量“每圈访问当前剩余矩形的一圈,边界内缩不重叠。”为什么能保证算法安全前进?
完整题目与题意拆解
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
在本站主例中,用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。
算法最终需要得到或观察:3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。
- • 输入:用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。
- • 机器需要维护:四边界变量、扫描方向、ans 输出序列。
- • 最终可观察结果:3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。
先看清算法到底要维护什么
先建立输入、目标、输出和第一批状态,不急着进入模板。
右→下→左→上,然后内圈
第一层方案:暴力做法
凭感觉逐格转向容易重复或漏掉边角;固定方向和边界收缩保证每个格子恰好处理一次。
暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。
重复工作究竟发生在哪里
把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。
右→下→左→上,然后内圈
整体地图:先做什么,再做什么
- 1建模把输入翻译成“矩阵边界框”,明确答案需要观察什么。
- 2状态只维护 四边界变量、扫描方向、ans 输出序列。
- 3转移每一步按照 每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
- 4收尾读取 3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。,并复核边界与复杂度。
矩阵边界框:核心概念
每走完一条边,只收紧刚刚完成的那一侧边界。
这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:四边界变量、扫描方向、ans 输出序列。
- • 每圈访问当前剩余矩形的一圈,边界内缩不重叠。
建立“矩阵边界框”心智模型
用主例建立核心状态,先预测下一步,再公开正确分支和理由。
右→下→左→上,然后内圈
核心机制:状态如何一步步变化
给出一个二维数组,按照螺旋的方式输出 解法一:需要注意的是特殊情况,比如二维数组退化成一维或者一列或者一个元素。注意了这些情况,基本就可以一次通过了。 解法二:提前算出一共多少个元素,一圈一圈地遍历矩阵,停止条件就是遍历了所有元素(count == sum)
每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
执行过程中持续维护:四边界变量、扫描方向、ans 输出序列。
正确性依赖以下不变量:每圈访问当前剩余矩形的一圈,边界内缩不重叠。
面试时可以压缩为:模拟四方向+收缩边界,O(mn) 每个元素一次。
落到当前题,执行机制可以压缩为:每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。
一次状态转移为什么成立
集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。
输出 1
正确性证明:为什么不会漏答案
初始化:算法开始时,全部合法候选仍在状态表示范围内;每圈访问当前剩余矩形的一圈,边界内缩不重叠。
保持:执行“每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。”时,只删除已经能证明不可能的候选,并把新信息写回 四边界变量、扫描方向、ans 输出序列。
终止:没有待处理状态或达到命中条件时,当前可观察结果就是“3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。”。
完整执行过程
- 1题目与输入用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。 因为:每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
- 2四边界 top/bottom/left/right准备第一圈向右。 因为:每步沿当前边前进。
- 3访问 (0,1) = 2沿 top 行向右。 因为:第一条边。
- 4预测下一步先不要看下一帧——根据当前不变量,预测算法接下来会怎么动。 因为:主动预测会暴露你对不变量的真实理解,比被动看动画有效得多。
- 5访问 (0,3) = 4沿 top 行向右。 因为:第一条边。
- 6访问 (1,1) = 6沿 top 行向右。 因为:第一条边。
- 7螺旋序: 1 → 2 → 3 → 4 → 8 → 12 → 11 → 10 → 9 → 5 → 6 → 7边界收缩至空。 因为:O(mn) 模拟。
- 8收尾与复杂度3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。 因为:时间 O(m×n) · 空间 O(1)。每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
从输入完整走到可观察结果
从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。
右→下→左→上,然后内圈
把动画和 Go 代码逐行对应
代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。
让每个动作都落到 Go 分支
重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。
右→下→左→上,然后内圈
完整 Go 提交代码与最小测试
// 解法 1
func spiralOrder(matrix [][]int) []int {
if len(matrix) == 0 {
return []int{}
}
res := []int{}
if len(matrix) == 1 {
for i := 0; i < len(matrix[0]); i++ {
res = append(res, matrix[0][i])
}
return res
}
if len(matrix[0]) == 1 {
for i := 0; i < len(matrix); i++ {
res = append(res, matrix[i][0])
}
return res
}
visit, m, n, round, x, y, spDir := make([][]int, len(matrix)), len(matrix), len(matrix[0]), 0, 0, 0, [][]int{
{0, 1}, // 朝右
{1, 0}, // 朝下
{0, -1}, // 朝左
{-1, 0}, // 朝上
}
for i := 0; i < m; i++ {
visit[i] = make([]int, n)
}
visit[x][y] = 1
res = append(res, matrix[x][y])
for i := 0; i < m*n; i++ {
x += spDir[round%4][0]
y += spDir[round%4][1]
if (x == 0 && y == n-1) || (x == m-1 && y == n-1) || (y == 0 && x == m-1) {
round++
}
if visit[x][y] == 0 {
visit[x][y] = 1
res = append(res, matrix[x][y])
}
switch round % 4 {
case 0:
if y+1 <= n-1 && visit[x][y+1] == 1 {
round++
continue
}
case 1:
if x+1 <= m-1 && visit[x+1][y] == 1 {
round++
continue
}
case 2:
if y-1 >= 0 && visit[x][y-1] == 1 {
round++
continue
}
case 3:
if x-1 >= 0 && visit[x-1][y] == 1 {
round++
continue
}
}
}
return res
}
// 解法 2
func spiralOrder2(matrix [][]int) []int {
m := len(matrix)
if m == 0 {
return nil
}
n := len(matrix[0])
if n == 0 {
return nil
}
// top、left、right、bottom 分别是剩余区域的上、左、右、下的下标
top, left, bottom, right := 0, 0, m-1, n-1
count, sum := 0, m*n
res := []int{}
// 外层循环每次遍历一圈
for count < sum {
i, j := top, left
for j <= right && count < sum {
res = append(res, matrix[i][j])
count++
j++
}
i, j = top+1, right
for i <= bottom && count < sum {
res = append(res, matrix[i][j])
count++
i++
}
i, j = bottom, right-1
for j >= left && count < sum {
res = append(res, matrix[i][j])
count++
j--
}
i, j = bottom-1, left
for i > top && count < sum {
res = append(res, matrix[i][j])
count++
i--
}
// 进入到下一层
top, left, bottom, right = top+1, left+1, bottom-1, right-1
}
return res
}func main() {
// 1. 主例
// 输入:mode="spiral-matrix", matrix=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
// 期望:3×3 → [1,2,3,6,9,8,7,4,5];3×4 → [1,2,3,4,8,12,11,10,9,5,6,7]。
//
// 2. 失败 / 未命中
// 检查:最后一行/列未处理就结束,漏元素。
//
// 3. 边界
// 空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
//
// 4. 迁移
// LC59 螺旋矩阵 II;LC48 旋转图像
}Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。
正确性与复杂度
执行过程中只保留仍可能影响答案的状态。每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
额外状态主要用于维护:四边界变量、扫描方向、ans 输出序列。
- • 每圈访问当前剩余矩形的一圈,边界内缩不重叠。
最容易写错的地方
最后一行/列未处理就结束,漏元素。
必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。
最后复盘:带走逻辑链
- 1题意用 top/bottom/left/right 维护未扫描矩形,顺时针螺旋输出矩阵元素。
- 2重复凭感觉逐格转向容易重复或漏掉边角;固定方向和边界收缩保证每个格子恰好处理一次。
- 3优化每轮访问 top→right→bottom→left 四条边后向内收缩;扫 bottom/left 前检查 top<=bottom、left<=right 防重复。
- 4证明每圈访问当前剩余矩形的一圈,边界内缩不重叠。
- 5复杂度时间 O(m×n),空间 O(1)
- • LC59 螺旋矩阵 II
- • LC48 旋转图像