刷 LeetCode 最怕遇到哪种题?就是那种题目看一眼全会,代码一写全废的类型。螺旋矩阵就是其中的典型代表——逻辑听起来简单得很:按顺时针一圈一圈遍历矩阵,把元素依次输出。但真下手去写,边界条件、方向转换、循环终止条件,处处都是坑,稍不留神就死循环或者越界。
这道题在 LeetCode hot100 里排第 54 位,属于高频面试题。各大厂面试官尤其爱考它,原因很简单:它不考任何高深的算法知识——不需要动态规划,不需要回溯,不需要图论——就是单纯考你“能不能把一个过程想清楚,然后用代码精确地表达出来”。这种题目最能筛掉那些只背模板、不真正理解代码逻辑的候选人。
我自己前前后后把这道题写过好几遍,从最开始对着答案抄都抄不明白,到后来能闭着眼写出两种最优解法,中间踩了不少坑。这篇文章就把我对这道题的理解完整拆解一遍,从题目分析、两种主流解法的思路推导,到代码实现、易错点排查,一次性说透。不管你是刚刷题的新手,还是准备面试的老手,这篇都能给你一些参考。
1. 题目理解与考点分析
1.1 题目到底在问什么
先看题目描述:给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
举个例子:
code复制输入:matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
输出:[1, 2, 3, 6, 9, 8, 7, 4, 5]
再看一个非方阵的例子:
code复制输入:matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
输出:[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
第二个例子特别重要,它告诉你一个关键信息:题目不要求矩阵必须是的方阵。很多初学者下意识地把这个题当方阵做,结果一遇到 3×4 或者 4×3 的矩阵就出问题。这点后面会详细展开。
所谓“顺时针螺旋顺序”,就是沿着矩阵的外围走一圈,然后进入内层再走一圈,一直走到所有元素都被访问完。整个过程你可以想象成一条贪吃蛇,一直沿着一个方向前进,碰到边界或者已经走过的格子就右转 90 度,继续前进,直到无路可走。
1.2 这道题要考什么能力
从面试角度来说,这个题考察的核心能力有三个:
第一,把具象的运动过程抽象成代码逻辑的能力。你需要把“沿着边界走”“遇到障碍右转”这些自然语言描述,转化成一堆变量和条件的数学表达。这是编程的基础功。
第二,边界条件的处理能力。什么时候该转向?什么时候循环该结束?当矩阵只有一行、只有一列,甚至只有一个元素时怎么办?这些特殊情况都是考察重点。
第三,空间复杂度意识。能否做到 O(1) 的额外空间,而不是动不动就开一个新的二维数组来标记是否访问过?面试官通常希望你写出空间上更优的方案。
1.3 和 hot100 里其他矩阵题的关系
有意思的是,hot100 里矩阵相关的题不算少,但考察角度各不相同。比如“矩阵置零”考察的是标记技巧,用常数空间记录状态;“旋转图像”本质上是找规律,考下标映射;“搜索二维矩阵”其实是在考二分查找。而螺旋矩阵则纯粹是模拟过程,考验的是过程拆解和精确编码的能力。
在刷题顺序上,我建议把螺旋矩阵作为“模拟类”题目里的入门题来做。它能帮你在一个可控的小场景里反复练习“状态转换”“边界判断”这类高频操作,而且不会牵扯太多算法知识,适合训练基本功。做透这一道,后面遇到“螺旋矩阵 II”(LeetCode 59,要求按螺旋顺序填入 1 到 n²)、“螺旋遍历”的变体题,都会轻松很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:方向模拟法——最直观的思路
2.1 核心思想
先说最直观的思路:把螺旋遍历的过程模拟成一条蛇在矩阵里爬行。这条蛇有三个行为准则:
- 沿着当前方向前进,每走一步就输出一个元素;
- 如果下一步会走到矩阵外面,或者走到已经访问过的位置,就右转 90 度;
- 右转之后如果还是不能走,说明所有格子都已经遍历完了,终止循环。
这套思路用代码实现,需要四个关键变量:当前坐标 (row, col)、当前方向 dir、访问标记数组 visited、方向数组 dirs。
方向数组是核心。它按顺时针顺序定义了四个方向,比如:
code复制方位顺序:右 → 下 → 左 → 上
对应坐标变化:(0, 1) → (1, 0) → (0, -1) → (-1, 0)
为什么要按这个顺序定义?因为题目要求的是顺时针,而顺时针的移动顺序恰好就是“右→下→左→上→右”这样循环。当你需要转向时,只需要让当前方向索引 +1,再用 % 4 取模,就能得到下一个方向,简洁且不容易出错。
2.2 代码实现(Python 版)
python复制class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
visited = [[False] * n for _ in range(m)]
# 方向:右、下、左、上
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
dir_index = 0
res = []
row, col = 0, 0
for _ in range(m * n):
res.append(matrix[row][col])
visited[row][col] = True
# 计算下一步的位置
next_row = row + dirs[dir_index][0]
next_col = col + dirs[dir_index][1]
# 如果下一步越界或已访问,转向
if next_row < 0 or next_row >= m or next_col < 0 or next_col >= n or visited[next_row][next_col]:
dir_index = (dir_index + 1) % 4
next_row = row + dirs[dir_index][0]
next_col = col + dirs[dir_index][1]
row, col = next_row, next_col
return res
这段代码里最值得注意的就是循环次数的控制。你不需要在 while 循环里额外判断终止条件,而是直接循环 m × n 次——因为矩阵一共就 m × n 个元素,每个元素必然被访问一次且只被访问一次。这个写法避免了在 while 循环里判断“下一步是否还能走”的麻烦,大大降低了死循环的风险。
2.3 时间复杂度与空间复杂度分析
方向模拟法的时间复杂度是 O(m × n),因为每个元素恰好被访问一次,方向数组的查找和转向操作都是 O(1)。
空间复杂度是 O(m × n),主要花在了 visited 数组上。你开了一个和原矩阵同样大小的二维数组来标记访问状态。这在 LeetCode 上能过,但不是最优的空间方案。面试时如果追问“能否优化到 O(1) 空间”,方向模拟法就不够了,需要用接下来要讲的边界收缩法。
这里我想多说一句:虽然 visited 数组额外占用了空间,但在实际写代码时,它带来的好处是逻辑异常清晰。尤其对于刚上手算法题的初学者,优先保证代码的正确性和易读性,再去谈优化,这是更合理的刷题策略。不要一上来就追求最优解,结果写半天写不出来,反而打击信心。
3. 解法二:边界收缩法——简洁高效的优化版
3.1 核心思想
边界收缩法的思路完全不同——它不模拟蛇的移动,而是直接维护四个边界值:top、bottom、left、right。每次遍历当前的外围一圈后,就把对应的边界向内收缩一格。比如遍历完最上面一行后,top 就加一;遍历完最右边一列后,right 就减一。如此循环,直到 top > bottom 或者 left > right,说明所有元素都已经遍历完毕。
具体遍历顺序是固定的:
- 从左到右遍历上边界那一行;
- 从上到下遍历右边界那一列;
- 从右到左遍历下边界那一行;
- 从下到上遍历左边界那一列。
然后再回到第 1 步,直到边界交错。
这里有一个非常容易被忽视的坑:不是每一步都需要执行。如果在第二步执行之前,top 和 bottom 已经重合了,说明矩阵只有一行,遍历完第一行就结束了,第三、四步就不能再执行。同理,如果 left 和 right 重合,说明矩阵只有一列,第二步执行完后就要小心第三步是否会重复遍历。
3.2 代码实现(Python 版)
python复制class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
top, bottom = 0, m - 1
left, right = 0, n - 1
res = []
while top <= bottom and left <= right:
# 1. 从左到右,遍历上边界
for col in range(left, right + 1):
res.append(matrix[top][col])
top += 1
# 2. 从上到下,遍历右边界
for row in range(top, bottom + 1):
res.append(matrix[row][right])
right -= 1
# 3. 从右到左,遍历下边界
if top <= bottom:
for col in range(right, left - 1, -1):
res.append(matrix[bottom][col])
bottom -= 1
# 4. 从下到上,遍历左边界
if left <= right:
for row in range(bottom, top - 1, -1):
res.append(matrix[row][left])
left += 1
return res
核心的坑就在第 3、4 步前的 if 判断。很多初版代码没有这两个 if,结果在遍历 3×4 这样的非方阵时,会把中间的元素重复输出。这是因为:收缩边界后,某一行或某一列可能已经被上一个步骤遍历过了,再执行下一步就会重复。
3.3 时间复杂度与空间复杂度分析
时间复杂度同样是 O(m × n),每个元素访问一次。空间复杂度是 O(1),因为只用到了几个整数变量记录边界状态,没有再开额外的数组。
为什么推荐在面试中用边界收缩法? 因为它的空间复杂度更优,代码也更紧凑,而且不需要额外的访问标记数组。面试官一看就知道你对这个题有深入理解,不是背答案。如果你的目标是 LeetCode 刷题通过,方向模拟法完全够用;但如果目标是面试表现,我更建议熟练掌握边界收缩法。
4. 两种解法对比与实战建议
4.1 选哪种解法更好
直接说结论:刷题练手两种都写一遍;面试优先用边界收缩法。
方向模拟法的优点是“不用思考边界收缩的逻辑”,因为 visited 数组帮你挡住了所有“重复访问”的可能;缺点是空间复杂度高。边界收缩法的优点是空间 O(1),代码更优雅;缺点是每次遍历完一步要立刻更新边界,更新完之后还要注意顺序的边界判断,写错了容易出问题。
我个人的经验是:先写方向模拟法帮助理清“蛇是怎么走的”,再写边界收缩法帮助理解“为什么会这样做”。两种解法都写一遍,你对这道题的理解绝对比只背一种要深刻得多。
我这里也整理了一个对比表,方便你复习:
| 对比维度 | 方向模拟法 | 边界收缩法 |
|---|---|---|
| 核心思路 | 模拟蛇的移动和转向 | 维护四个边界,逐层收缩 |
| 额外空间 | O(m×n) visited 数组 | O(1) |
| 代码可读性 | 逻辑直白,易理解 | 边界判断稍多,但更紧凑 |
| 面试推荐度 | 适合新手理解 | 更推荐在面试中使用 |
| 易错点 | 方向索引取模,循环次数 | 边界更新后,单行/单列的重复遍历 |
4.2 一个更取巧的解法(Python 一行思路)
额外分享一个小技巧:Python 里有一种“旋转”式的解法,代码很简洁,但面试时不建议主动用,因为可读性差,而且依赖 Python 的切片特性,其他语言没法迁移。
python复制class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
while matrix:
res += matrix.pop(0)
if matrix and matrix[0]:
for row in matrix:
res.append(row.pop())
if matrix and matrix[0]:
res += matrix.pop()[::-1]
if matrix and matrix[0]:
for row in matrix[::-1]:
res.append(row.pop(0))
return res
思路就是:每次遍历外圈,然后把矩阵从外到内剥一层。代码写起来很爽,但实际工作中除非你有明确的性能瓶颈分析,否则不建议这么写——这种代码是典型的“不可维护代码”,过两个月你自己回来看都费劲。刷题时用来练手可以,面试时主动写这种容易被面试官质疑代码风格,得不偿失。
5. 高频踩坑与调试技巧
5.1 最容易翻车的几种情况
这道题的高频错误其实非常集中,我总结了三大类。
第一类,死循环。当你用 while 循环而不是 for _ in range(m*n) 来控制遍历次数时,很容易因为转向条件判断不对,导致蛇在某一个圈里反复转圈。典型的场景是:visited 检查写成了 visited[next_row][next_col] == False 才走,但忘了先判断越界。在 Python 里,索引越界直接抛异常;在 C++/Java 里则可能出现未定义行为。
第二类,重复输出。在边界收缩法中,如果你在第 3、4 步之前不加 if 判断,当矩阵只有一行或只有一列时,会重复输出已经遍历过的元素。比如 matrix = [[1, 2, 3, 4]],正确输出是 [1, 2, 3, 4];不加 if 的版本会输出 [1, 2, 3, 4, 3, 2, 1],多了一段。
第三类,空矩阵。题目虽然限定 m >= 1,但如果你把代码抽象成函数给别人调用,或者自己在调试时传入 matrix = [],那么 len(matrix[0]) 就会直接越界报错。稳妥的做法是在函数开头加一行判空:
python复制if not matrix or not matrix[0]:
return []
5.2 调试技巧与自查方法
调试这类过程模拟的题目,最有效的手段是“小规模矩阵手推”。不要急着跑代码,你先拿一个 3×3 的矩阵,自己在纸上把每一步 top、bottom、left、right 的值和当前输出的元素写出来,然后和代码的执行结果对照。一旦代码输出和手推不一致,你很容易就能定位是哪一步的逻辑出了问题。
更实用的排查手法是先拿几种特殊形状的矩阵来测试:
- 单行:
[[1, 2, 3, 4]] - 单列:
[[1], [2], [3], [4]] - 单元素:
[[1]] - 长条矩形:3 行 5 列
- 高条矩形:5 行 3 列
把这五种形态的矩阵都跑一遍,如果输出都正确,基本可以说明你的代码没问题了。这是我刷矩阵类题目一直沿用的一套“自测清单”,强烈推荐你也养成这种习惯。
5.3 一个易被忽略的细节:方向数组的取模运算
在方向模拟法中,dir_index = (dir_index + 1) % 4 这行代码看似简单,但它背后藏着循环结构设计的一个小技巧。取模保证了方向索引始终在 0、1、2、3 这个范围内循环,不会越界。如果你写成了 dir_index += 1,那到第四次转向时方向索引变成 4,dirs[4] 就直接越界报错了。
我见过不少人在面试时因为这个细节被提醒,属于“会做但写不严谨”的典型扣分项。平时写代码要养成这种对“越界风险”的本能敏感。
6. 从螺旋矩阵到算法思维的延伸
6.1 为什么这道题值得反复刷
螺旋矩阵这道题,本质上考察的是一个非常重要的编程能力:把复杂过程分解成有规律的、可复现的步骤。这种能力在真实开发中无处不在——比如游戏开发中地图的区块遍历,图像处理中的像素扫描路径规划,甚至日志分析中按某种规则读取多维数据,都会遇到类似的过程模拟问题。
我在实际的项目里就遇到过类似场景:一个数据可视化工具需要按螺旋顺序读取一个二维矩阵的数值,用来生成从中心向外扩散的热力图。当时就是套用螺旋矩阵的思路快速实现的。所以说这道题不是纯刷题用,它是真的有工程价值的。
从刷题角度来说,这道题也适合作为“过程模拟”类题目的重难点突破。把螺旋矩阵吃透,再去做“螺旋矩阵 II”“旋转图像”“对角线遍历”这类题目,你会发现在过程拆解和边界处理上有很多共通之处,一通百通。
6.2 从这题延伸出去的几个思考
把这题做透之后,我建议你带着下面几个问题再去看一些题,会有意外的收获:
- “螺旋矩阵 II”要求按顺时针顺序把 1 到 n² 填入矩阵,本质上是一样的过程,只不过从“读”变成了“写”。你能用边界收缩法直接改出来吗?
- 如果把方向从顺时针改成逆时针,代码需要改哪些地方?
- 如果要你按螺旋顺序遍历一个稀疏矩阵(很多地方是空的),方向模拟法还适用吗?空间上还有更优的方案吗?
这类“一题多问”的思考方式,比单纯刷题数量重要得多。我见过太多人 hot100 刷了三遍,但遇到原题变种还是不会做,就是因为只背了答案,没有理解过程。
6.3 刷题建议与节奏把握
最后聊聊刷题节奏的问题。LeetCode hot100 算是每个算法学习者的必经之路,但我不建议你盲目追求“一天十题”的进度刷完一遍就宣告完成。像螺旋矩阵这种让你“感觉会做、但写不对”的题,恰恰是刷题中最有价值的部分——它暴露了你对边界条件把控的薄弱点。
我的建议是:第一遍刷题时可以看题解,但看完之后一定要自己重新写一遍,并且把几种特殊输入都跑一遍;过两周再独立写一遍,直到能流畅写出才叫真正掌握。那些“看了答案能懂、自己写写不出”的题,才是该多刷几遍的重点。螺旋矩阵就是这样一道值得二刷、三刷的经典题。
回到开头说的——“看了全会,写了全废”,本质上不是你不会,而是缺少对细节的掌控。把这题吃透,把方向模拟法和边界收缩法都印进脑子里,以后遇到任何“按规则遍历”的题目,你都会有一种莫名的底气。这也是刷题真正的意义:不是背下几百道题的答案,而是培养一种精确、严谨地思考过程的能力。
