近半年来我刷 Hot100 练手,几乎每到数组模拟类的题目都能看到评论区在吵“螺旋矩阵到底算 easy 还是 medium”。如果你也卡在这题超过二十分钟,大概率不是不会遍历,而是“转着转着就不知道自己转到哪了”。这篇就把 54.螺旋矩阵 从题目本质、四种解法、边界坑到面试策略一次性讲透,顺便把它和 Hot100 里其他网格题的关联串起来。
先说一下这题是什么:给你一个 m 行 n 列的二维矩阵,按顺时针螺旋顺序返回所有元素。听起来就是“外层一圈、内层一圈”地剥洋葱,但真正手写的时候,最让你头疼的不是算法思想,而是上下左右四个边界在循环里怎么收缩、什么时候该收缩、什么时候该停。这篇文章适合两类人:一类是正在跟 Hot100 死磕的刷题党,另一类是把算法题当成面试基本功、想搞清楚“为什么我写出了死循环”的求职者。我会把代码逐行拆开,把容易翻车的细节全部摊开来讲。
1. 先别急着写代码:把“螺旋”抽象成边界收缩问题
1.1 题目到底在考什么
很多人第一眼看到螺旋矩阵,直觉是“模拟走迷宫”,于是立刻开两个变量记录当前行列坐标,再用方向数组控制上下左右。这个思路本身没错,但很容易走进一个陷阱:你把问题想成了“一条蛇在网格里散步”,却忽略了螺旋的本质是不断缩小的矩形区域。
如果从边界角度看,螺旋遍历可以描述成四个阶段循环:
- 在当前的 top 行,从左往右走完整个矩形上边;
- 在当前的 right 列,从上往下走完矩形右边;
- 在当前的 bottom 行,从右往左走完矩形下边;
- 在当前的 left 列,从下往上走完矩形左边。
走完一轮之后,矩形向内缩了一圈,也就是 top 加一、bottom 减一、left 加一、right 减一,然后重复。只要矩形还存在,也就是 top <= bottom 且 left <= right,就继续。
这个视角的价值在于:它把“螺旋”降维成了“反复遍历四条直线”。你不用再关心当前位置是不是已经访问过,也不用担心蛇头会不会撞墙,因为每一步都限定在明确的区间里。我在实际刷题中体会很深的一点是:凡是把螺旋矩阵写复杂的人,都是因为没有明确“边界”才是唯一的约束条件。
1.2 “剥洋葱”模型:为什么边界法最贴合直觉
用生活化类比来解释,螺旋矩阵就是一片洋葱。你从最外层剥起,剥完一圈,看到的是剩下的一层更小的洋葱。对计算机来说,“剥”这个动作对应的是 top 往下移、bottom 往上移、left 往右移、right 往左移。当洋葱剥到只剩一层甚至只剩一格的时候,关键问题是:这一圈有没有可能退化成一条线?
这就是 54 题和普通遍历最大的区别:每一轮循环中,四个边并不是永远都存在。当矩形退化成一横行时,你只需要上边和下边,但下边和上边其实是同一条线,如果盲目把四条边都走一遍,就会重复输出元素;当矩形退化成一竖列时,左列和右列也是同一条线,同样也会重复。很多人的死循环和重复元素问题,根源都在这里。
记住这张图,后面的代码就好理解了:xxx这个抽象的矩形里,螺旋遍历按“上边、右边、下边、左边”的顺序切一圈,每切完一圈边界向中心收缩。如果某一时刻矩形退化成线或点,就要小心别把同一条线走两遍。
2. 四边界模拟法:最稳妥的 medium 题标准答案
2.1 手动推演一遍 3×4 矩阵
光说概念太虚,我拿一个 3 行 4 列矩阵来手动走一遍,矩阵内容如下:
1 2 3 4 5 6 7 8 9 10 11 12初始状态:top = 0,bottom = 2,left = 0,right = 3。
- 先走 top 行,从 left 到 right,输出 1、2、3、4,然后 top 变成 1。
- 再走 right 列,从 top 到 bottom,也就是从第 1 行到第 2 行,输出 8、12,然后 right 变成 2。
- 再走 bottom 行,从 right 到 left,输出 11、10、9,然后 bottom 变成 1。
- 再走 left 列,从 bottom 到 top,从第 1 行到第 1 行,输出 5,然后 left 变成 1。
- 此时 top = 1,bottom = 1,left = 1,right = 2,矩形还有一层,继续循环。
- 走 top 行,输出 6、7,top 变成 2。注意此时 top 已经大于 bottom,剩下四条边理论上还有 right 列可以从 top 走到 bottom,但 nothing 发现 top > bottom,所以右边不应该再走、下边也不应该再走、左边更不应该走。
如果你不判断“当前是否还有必要走第三边和第四边”,在最后这步你就会把 7 再输出一遍或者干脆横冲直撞冲出去。这个简单的例子已经把边界判断的必要性暴露无遗。
2.2 完整代码与逐行注释
下面是四边界模拟法的 Python 实现,建议直接背下来当成模板:
def spiralOrder(matrix): res = [] if not matrix or not matrix[0]: return res top, bottom = 0, len(matrix) - 1 left, right = 0, len(matrix[0]) - 1 while top <= bottom and left <= right: # 第一边:从左到右遍历上边 for j in range(left, right + 1): res.append(matrix[top][j]) top += 1 # 第二边:从上到下遍历右边 for i in range(top, bottom + 1): res.append(matrix[i][right]) right -= 1 # 第三边:从右到左遍历下边 if top <= bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 # 第四边:从下到上遍历左边 if left <= right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res这里有几个细节值得单独解释:
- 为什么第一边和第二边不需要判断?因为 while 条件已经保证了 top <= bottom 且 left <= right,第一边有 left 到 right 的区间可走;第二边在 top 更新后,仍然有 top <= bottom 保证可走。但第三边存在的前提是“上边移动完之后,矩形还没消失”,第四边存在的前提是“左边收缩之后,矩形还没消失”。
- 边界更新的顺序不能乱:每走完一条边,对应的边界必须立刻收缩。如果你把 top += 1 放到最后统一处理,区间会算错。
- while 循环的条件用 top <= bottom and left <= right,只要矩形还在,就继续剥皮。
这个解法的时间复杂度是 O(m*n),因为每个元素恰好被访问一次;空间复杂度是 O(1),不包括存储结果的空间。这也是面试中最容易量化说明的部分。
2.3 三种常见写法对比
在我看过的题解里,四边界法还存在三种典型的代码形态,它们的正确性都对,但可读性和出错率差距很大。
第一种就是我上面写的这种“if 判断 + 四条边独立”的版本,清晰、不易错,强烈推荐。第二种是很多 C++ 题解会写的“四个 while 循环嵌套”版本,它的思路是每走一个方向就把边界收缩,但里层循环的条件往往写得冗长,而且一旦矩阵是单行或单列,很容易出现空转死循环。第三种是“while True + break”版本,每次循环先走四边,如果某一边的起点已经越过终点,就 break 掉。这种写法更简洁,但 break 的位置和判断条件不好记,在面试压力下容易漏掉。
我用一个简短表格帮你对比:
| 写法 | 核心思路 | 优点 | 隐患 |
|---|---|---|---|
| if 判断 + 四边独立 | 每条边遍历后立即收缩边界,后两边加 if | 可读性最高,不易重复、不易死循环 | 代码稍长 |
| 四个 while 嵌套 | 每个方向一个 while 循环 | 逻辑紧凑 | 单行单列容易空转 |
| while True + break | 每轮走四边,判定边界重叠就中断 | 代码最短 | break 位置记不住,容易漏条件 |
实战中我优先推荐第一种,因为它的错误率最低。面试官让你讲思路时,你也可以直说“我选择每轮处理四条边,但第三边和第四边需要单独判断边界是否重叠”,这句话基本能把你的思路说清楚。
3. 方向数组 + 访问标记:另一种看似绕、其实很强的解法
3.1 方向数组的本质:用“下一步预判”替代“边界收缩”
四边界法是从矩形负责人视角看的,而方向数组模拟法是从行走者视角看的。你有一个方向,比如初始向右,每一步移动之前先计算“如果按当前方向走下一步,位置是否合法”,合法就继续走;不合法,就把方向顺时针旋转 90 度,再重新计算下一步。
“非法”的判断标准有两个:一是越界,二是下一个格子已经访问过。这两个条件合在一起,决定了什么时候转向。代码可以这么写:
def spiralOrder(matrix): if not matrix or not matrix[0]: return [] m, n = len(matrix), len(matrix[0]) total = m * n visited = [[False] * n for _ in range(m)] res = [] # 方向按“下标的增量”表示:右、下、左、上 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] d = 0 r, c = 0, 0 for _ in range(total): res.append(matrix[r][c]) visited[r][c] = True nr, nc = r + directions[d][0], c + directions[d][1] if nr < 0 or nr >= m or nc < 0 or nc >= n or visited[nr][nc]: d = (d + 1) % 4 nr, nc = r + directions[d][0], c + directions[d][1] r, c = nr, nc return res这段代码的思路非常优雅:你不用手动管 top、bottom、left、right 这些变量,只需要一个 visited 数组和一个方向索引。每走一步,先尝试继续直行,如果直行不可达,就转向。因为每轮都移动一步,一共循环 m*n 次,所以必然遍历完所有元素。
我特别欣赏方向数组的一点是:它对“螺旋”的还原度很高,人走路就是这样的——撞墙了才拐弯。四边界法定义的是“我负责把这个圈剥完”,方向数组法定义的是“我负责让自己永不撞墙”。
3.2 什么时候该在面试里选它
方向数组法的最大优点是扩展性强。如果你想改成逆时针螺旋,只需要把 directions 换成 [(0,1), (-1,0), (0,-1), (1,0)] 即可;如果你想改成一个“碰壁转向”的网格搜索,它可以直接套用;如果你后面再做 59.螺旋矩阵 II(按螺旋顺序填充矩阵),这个 visited + 方向数组的思路也能无缝切换。
但它的缺点也很明显:需要一个额外的二维数组 visited,空间复杂度变成 O(m*n)。面试官如果追问“能不能优化到 O(1)”,你得能回答出“可以,但要放弃 visited,改用边界判断,或者走完边就收缩边界,即四边界法”。这就是面试中最好的展示节奏:先给出方向数组解法,再提出可以优化空间,然后切换到四边界写法。
我个人建议:如果你对这题已经足够熟练,可以直接回答四边界法,把 O(1) 空间作为亮点;如果你是在压力面中临时遇到这题,方向数组反而更不容易写错,因为它不需要你记住复杂的边界收缩时机。两者并不矛盾,多掌握一种写法,面试时就多一重保险。
4. 容易翻车的边界细节,全在这里了
4.1 单行单列矩阵:死循环的经典源头
如果你拿四边界法但忘了第三边、第四边的判断,那么在下面两个用例上必然出问题:
输入: [[1, 2, 3, 4]] 预期: [1, 2, 3, 4]初始 top = 0,bottom = 0,left = 0,right = 3。第一边遍历完 top 行后,top += 1,变成 1。此时 while 条件 top <= bottom 已经不成立,所以根本进不去下一轮循环。第一版代码不会出问题,因为 while 条件卡住了。
但如果你写的是“四个 while 循环嵌套”,第一边的循环结束后进入第二边的 while,条件是 top <= bottom,也就是 1 <= 0,循环不会执行,但如果你没有在外面套 while top <= bottom and left <= right,而是直接用四个 while 依次执行,你就会发现第二边条件已经不满足,第三边和第四边还在按旧边界执行,于是要么输出错乱,要么死循环。
竖列用例同理:
输入: [[1], [2], [3], [4]] 预期: [1, 2, 3, 4]处理到第二边时 right -= 1,右边变成 0,left 还是 0,此时左列和右列重叠,如果第三边还去走 bottom 行,就会把倒数第二个元素重复输出。
避坑口诀:每完成一条边立刻收缩对应边界,第三边和第四边执行前必须检查矩形是否仍然存在。
4.2 重复遍历根因:每次收缩后的 while 顺序
还有一个特别容易踩的坑:四个边界收缩的顺序到底能不能换?
我用一个反例说明:假定你先收缩 top,再收缩 right,在走第三边 bottom 行时,range(right, left - 1, -1) 里的 right 是已经收缩过的,这是对的。但如果你不小心在第三边之前就执行了 bottom -= 1,那第三边遍历到的就不再是你想要的尾部行。所以边界收缩和对应边的遍历必须紧密耦合,顺序不能打散。
此外,检查 while top <= bottom and left <= right 放在一轮循环的开头还不够,因为在第一边、第二边执行完之后,矩形可能退化成“一条竖线”,这时第三边、第四边就不该再执行。所以第三边和第四边必须有独立的 if 检查,而不是只靠外层 while。这两个 if 是我在实际笔试中最容易漏掉的部分,漏掉的后果就是样例对了但提交全红。
4.3 内置 rotate 的“聪明”写法为什么不推荐
如果你在网上搜索螺旋矩阵,会看到一种很短的解法:
def spiralOrder(matrix): res = [] while matrix: res += matrix.pop(0) if matrix and matrix[0]: matrix = [list(row) for row in zip(*matrix)][::-1] return res它的思路是:每次把第一行取走,然后对剩下的矩阵逆时针旋转 90 度,这样新的“第一行”就是原本的右边列,只要不断循环就能剥完整个螺旋。代码确实很短,但我不推荐在重要场合使用它。
原因有三点:第一,zip(*matrix)和矩阵转置的写法依赖 Python 的特性,在 C++ 或者 Java 中很难这样简洁复现;第二,每轮都要矩阵旋转,涉及大量元素搬移,虽然均摊复杂度依然是 O(m*n),但常数很大;第三,面试官很容易追问“如果每轮都 rotate,空间开销怎样”,你解释起来会非常被动。我见过有人把这题用 rotate 三行写完,结果面试官让他改成只能 O(1) 空间的版本,当场卡住。
所以这种写法可以作为知识面展示,也可以用于快速验证自己的结果是否和标准答案一致,但真正面试答题时别用。
5. 复杂度与面试策略: medium 题的得分逻辑
5.1 时间/空间复杂度推导
这题的标准复杂度非常固定:
- 时间:每个元素被访问一次,所以是 O(m*n),其中 m 是行数,n 是列数。
- 空间:四边界法的辅助空间是 O(1);方向数组法的辅助空间是 O(m*n),因为 visited 矩阵。
面试官问“能不能优化空间”时,你要能说出四边界法的边界变量只有四个,所以达到了理论下界。结果数组 res 是题目要求返回的,不计入辅助空间。
这些其实不难,但很多人会在面试时答成 O(n),忘了说明 n 代表什么。我建议你养成习惯:说清楚 m、n 分别是什么,然后写 O(m*n),这样不会留下“二维题只考虑一维”的坏印象。
5.2 面试官希望你展示什么
medium 题在面试中的定位是“考察你对控制流的掌控”。面试官绝大多数情况下不会在乎你是否记得标准答案,更在意你遇到边界情况时怎么反应。换句话说,你能得出正确答案只是及格线,你能不能清楚解释“为什么第三边要加 if 判断”才是拉开差距的地方。
我的建议是:在写完代码之后,主动说出三组测试用例:
- 3×4 矩阵,验证常规情况;
- 1×5 矩阵,验证单行情况;
- 5×1 矩阵,验证单列情况。
如果你能当面把这三组用例手推一遍,甚至还没等面试官提问就主动说明“单行单列时要注意第三边和第四边不存在”,那么这个 medium 题基本就是稳了。面试官看重的不是“我写过 100 遍这题”,而是“我理解边界背后的几何含义”。
还有个细节:如果你第一遍写的代码是对的,但中间犹豫了,面试官很可能会追问“如果让方向数组法来做,你怎么保证不重不漏”,这时候你可以从“每次转向都由 visited 或越界触发”来回答。整个答题过程如果能做到“代码 + 复杂度 + 边界案例”三件套,就非常完整了。
6. 从螺旋矩阵延伸到整个 Hot100:一个题串起一片网
6.1 同类型题:螺旋矩阵 II 与旋转图像
Hot100 里和螺旋矩阵关联最直接的题是 59.螺旋矩阵 II:给你一个正整数 n,生成一个按顺时针螺旋排列的 n×n 矩阵。这题和 54 题几乎是同一套模板,只是把“读取”换成“填充”。套用四边界法,把 res.append 改成按计数器依次赋值给矩阵对应位置即可。
还有 48.旋转图像,它的核心做法是“先按对角线转置,再逐行翻转”,也可以理解为矩阵边界操作的热身。如果你已经熟练螺旋矩阵的边界收缩,再看旋转图像就会觉得“矩阵的几何变换其实就是一堆坐标映射”,而不是死记硬背公式。
Hot100 里这类“网格模拟”是一个小主题,它们共同强调一件事:二维数组的题目难点从来不是语法,而是你能否在脑中建立“坐标 + 边界”的模型。把一道螺旋矩阵吃透,再去做其他矩阵模拟题,你会发现很多题的边界思想是相通的。
6.2 蛇形矩阵等变体
面试中除了螺旋矩阵,还有一个常见的变体是“蛇形矩阵”或者“对角线遍历”。比如按对角线顺序输出矩阵元素,或按“之”字形一行行输出。这类题本质都是换一种边界扫描顺序,但判断条件往往更复杂。
例如蛇形矩阵,你需要用一个布尔变量表示当前是从左到右还是从右到左,并在每行结束时切换。如果你已经掌握了边界收缩的思想,这些变体都不难理解,因为它们只是“螺旋”的某一环变形而已。我在刷题群里看到有人把螺旋矩阵 II、蛇形矩阵、旋转图像放在一天之内刷,效率奇高,就是因为它们的底层控制流模型高度相似。
6.3 热词里那些题的共性考点
除了矩阵模拟,Hot100 里还有大量和“状态转移”相关的题目,比如腐烂的橘子、基本计算器、爱吃香蕉的人等。它们表面看起来完全不同,但底层的思维方式有共通之处:一个状态机、一个边界条件、一个逐步推进的循环。
- 腐烂的橘子本质是 BFS 的分层扩散,你需要在网格里维护“新鲜橘子”的数量,并严格按照分钟数逐层扩散;
- 基本计算器本质是状态机解析,需要关注数字、运算符、括号之间的状态切换;
- 爱吃香蕉的人本质是二分答案,对吃速做二分搜索,判断能否在指定时间内吃完。
如果你把螺旋矩阵理解成“边界 + 方向 + 状态切换”,那么这些题其实都在训练同一套能力:把抽象规则转化成循环控制条件的能力。这也是为什么碰到这些题时,我会建议大家别只追求 AC,而是把思考过程写下来——因为考试不会出原题,但会出同一套思维的换皮题。
7. 我刷这题时踩过的坑与最终习惯
这题我从第一次 WA(Wrong Answer)到后来能闭眼写出来,大约经历了三个阶段。第一阶段是“看了解析觉得自己懂了”,一写就错,大部分错在单行单列上;第二阶段是“记住了四边界法模板”,但碰到 3×3 这种奇数矩阵时,最后一轮循环还是会产生重复输出;第三阶段才真正理解“边界收缩的意义是让几何区域逐步消失,而不是让循环变量自己瞎跑”。
我现在刷这题的习惯是:不管用什么语言写,先想清楚三件事——当前矩形的四个角坐标是什么、哪些边在这个状态下物理存在、每条边的遍历方向是从哪到哪。然后我会在纸上画一个 3×3 矩阵,从第一个元素开始一步步写出轨迹。这个动作大约需要三十秒,但却能帮我避免 90% 的边界错误。
如果你正在刷题,我特别建议你也养成这个“画轨迹”的习惯。不要相信脑内模拟,一定要落笔或者用注释写出来。因为螺旋矩阵的坑不在于算法复杂,而在于人的工作记忆容量有限,一旦循环次数超过三层,就很容易漏掉某条边的更新。画出来之后,代码反而变得异常简单——它变成了对一个几何过程的忠实翻译。
最后分享一个小技巧:如果你不确定自己写的四边界法是否正确,可以先用while True版本快速验证逻辑,再改写成while top <= bottom and left <= right版本。我在本地练习时经常用这个思路对比两种写法的输出,发现大多数错误都集中在“边界收缩后矩形退化”的那一步。把这些边界情况试完,这题才算真正过关。