作为刷完hot100接近三遍的人,螺旋矩阵这道题在我心里的地位一直很特殊。它不像动态规划那样需要复杂的推导,也不像图论那样需要精巧的算法设计,它就是一道纯粹的“模拟题”,但每次写,都有人会在边界条件上栽跟头。甚至可以说,这道题是面试中检验候选人“代码基本功是否扎实”的一道非常典型的分水岭。你问一个刷题量过百的人会不会做,他多半会说会,但你让他十五分钟内在白板上写出来且一次通过,很多人会卡壳。
这篇文章,我就把这题从题意理解到代码实现,再到面试考场上可能遇到的变体和追问,整个过程掰开揉碎讲清楚。尤其是那些“为什么这么写”和“哪里容易错”的细节,我会重点讲。
1. 这题到底在考什么:理解“模拟遍历”与“边界收缩”的本质
螺旋矩阵,全称是“螺旋矩阵 Spiral Matrix”,题目本身并不复杂:给你一个 m 行 n 列的矩阵 matrix,要求按照顺时针螺旋顺序,返回矩阵中的所有元素。
举个最典型的例子,输入三维矩阵:
text复制[[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]]
期望的输出是 [1, 2, 3, 6, 9, 8, 7, 4, 5]。
从箭头的走向来看,就是从外圈到内圈,像剥洋葱一样,一层一层往里走。这个走向从视觉上很好理解,但一旦落到代码上,你会发现难点不在于“理解题意”,而在于用代码“描述这次行走”。
1.1 为什么这道题被归类为“模拟”
LeetCode hot100 的题目,基本分为几大类:动态规划、深度优先搜索、广度优先搜索、双指针、滑动窗口、哈希表、贪心等,而螺旋矩阵在类型上属于“数组”和“模拟”。
所谓“模拟”,就是题目本身并没有隐藏在背后的数学公式,也不需要复杂的算法设计,你需要做的就是按题目描述的步骤,把每一步的行走路径忠实地用代码翻译出来。因此,这道题对代码的“精细度”要求特别高,尤其是控制循环边界和变量更新时,稍有疏忽就会导致索引越界或者漏元素。
类似“模拟”风格的题还有“斐波那契数列”(虽然它能用矩阵快速幂优化),以及 hot100 里的“旋转图像”。这些题的特点都是一眼能看懂,但下手写代码时会发现细节极多。
1.2 解决螺旋矩阵的两条主流思路
我见过、也用过两类主要解法:
第一类:按层模拟(边界收缩法)
这是最正统、最容易让人理解的方法。核心思想是用四个变量记录当前未遍历区域的上下左右边界:top、bottom、left、right。然后在一个循环里:
- 先从左到右遍历上边界那一行,遍历完
top++; - 再从上到下遍历右边界那一列,遍历完
right--; - 然后从右到左遍历下边界那一行,遍历完
bottom--; - 最后从下到上遍历左边界那一列,遍历完
left++。
每完成一轮,四个边界就往内收缩一圈,直到边界交错,说明全部元素都已遍历。
第二类:方向数组 + 访问标记法
用两个方向数组模拟“行走”的向量,比如 dirs = [(0,1), (1,0), (0,-1), (-1,0)] 分别代表右、下、左、上。每次前进时,先判断下一步是否越界,或者是否走进已访问过的格子,如果是,就切换方向。同时维护一个 visited 二维数组做标记。
这类方法代码结构上更像“机器人走路”,理解起来直觉性也很强,但需要额外的 O(m*n) 空间来记录访问状态。与之相比,第一类边界收缩法只需要常数级别额外空间,空间复杂度上是更优的。
我在面试中推荐优先掌握第一类方法,不是说第二类不对,而是第一类让你对矩阵边界的理解更深入,且空间复杂度更漂亮。当然,两种方法都值得练一练,在写变体题时会交替用到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 边界收缩法的代码骨架:从“思路”到“实现”的精细工程
理解了核心思路是“剥洋葱”之后,我们直接看代码实现。这里我给出最简洁也最容易理解的 Python 写法,然后逐段拆解。
python复制from typing import List
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix or not matrix[0]:
return []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 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
这段代码看起来很短,但里面有几个非常关键的细节,决定了你写的版本是“正确但偶有 bug”,还是“稳稳一次通过”。
2.1 循环条件:为什么是 while top <= bottom and left <= right
很多同学第一次写时,习惯写成 while top < bottom and left < right,然后就会发现输出少了最中间的一行或一列。问题的根源在于对奇偶数行/列的边界情况考虑不周。
举个例子,矩阵是 3×3 时,经过第一轮收缩后:
- 初始:
top=0, bottom=2, left=0, right=2 - 第一轮结束:
top=1, bottom=1, left=1, right=1
此时如果循环条件是 top < bottom and left < right,则 1 < 1 为 false,循环退出,中间的元素 5 就被漏掉了。而用 <= 则能保证最后一轮只剩一行或一列甚至一个元素时,继续进入循环处理。
2.2 收缩边界的顺序与去重陷阱
每一轮的四步遍历,顺序是固定的:上 → 右 → 下 → 左。但关键在于,并不是每一轮四步都需要完整执行。当只剩一行或一列时,部分方向会导致重复遍历。
比如矩阵是单行:[[1, 2, 3, 4]]。
- 初始:
top=0, bottom=0, left=0, right=3 - 第一步“从左到右”遍历完 [1,2,3,4],
top变成 1 - 此时
top=1, bottom=0,循环条件1 <= 0为 false,所以整个循环退出,结果是正确的[1,2,3,4]
再看矩阵是单列:[[1], [2], [3]]。
- 初始:
top=0, bottom=2, left=0, right=0 - 第一步遍历 matrix[0][0],得 1,
top=1 - 第二步“从上到下”遍历 row=1、2,得 2、3,
right=(-1) - 循环条件
left <= right即0 <= -1为 false,退出,结果正确。
但如果中间不检查,就会出问题。比如一个 3×4 的矩阵,第一轮走完后,如果直接执行“从右到左遍历下边界”,而此时 top 已经大于 bottom,就会把上一轮已经取过的元素再取一遍。这就是为什么第 3 步和第 4 步前需要分别加 if top <= bottom 和 if left <= right 判断。
2.3 为什么不推荐用“坐标偏移 + visited”写法
从工程效率的角度讲,visited 数组需要额外空间,而且每走一步都要做越界和访问判断,增加代码分支。更重要的是,这类写法写多了之后,人的思维容易停留在“机器人感知周围环境”的层面,而不会主动去思考“矩阵的边界是一个不断内缩的变量”。
这也解释了为什么很多面试官偏爱边界收缩法——它体现的是一个人对问题结构的抽象能力:你能不能把这整个遍历过程,看作是对四个边界动态维护的过程,而不是跟着元素一个一个走。
3. 从 54 题到相关变题:面试官为什么总爱在这题上做文章
刷题刷多了你会发现,LeetCode 的题并不是孤立的。螺旋矩阵这道题在 hot100 里的位置虽然偏后,但它的变体和相关性极高,面试官只需要改动一个条件,就能衍生出好几道新题。
3.1 变体一:输出螺旋矩阵的第 k 个元素
面试官可能会问:如果矩阵特别大,我不想遍历完整个矩阵,只想知道螺旋顺序下第 k 个元素是什么,能不全遍历吗?
这时边界收缩法就能演化成“按圈数定位”的数学方法。你首先要确定第 k 个元素在哪个圈(第几层),然后计算它在该圈的哪条边上。这个方法需要用到每圈元素数量的通项公式,反而成了数学题。
不过这类变体在面试中出现的概率偏低,因为计算层面稍显复杂。如果被问到,至少你要能立刻反应过来,沿着“一圈一圈跳过”的思路去想。
3.2 变体二:生成螺旋矩阵(59. Spiral Matrix II)
这是我最常被问到的变体,也是 hot100 里“螺旋矩阵”最直接的姊妹题。题目要求给你一个 n,生成一个从 1 到 n² 的螺旋矩阵。换句话说,从“按螺旋顺序取出”变成“按螺旋顺序填入”。
代码逻辑几乎和 54 题一模一样,只是把 res.append(matrix[top][col]) 换成 matrix[top][col] = val; val++。我在刷题时经常把这两道题放到一起做,每做完 54 题就顺手刷一遍 59 题,目的就是巩固边界收缩的流畅度。
python复制def generateMatrix(n: int) -> List[List[int]]:
matrix = [[0] * n for _ in range(n)]
top, bottom, left, right = 0, n - 1, 0, n - 1
val = 1
while top <= bottom and left <= right:
for col in range(left, right + 1):
matrix[top][col] = val
val += 1
top += 1
for row in range(top, bottom + 1):
matrix[row][right] = val
val += 1
right -= 1
if top <= bottom:
for col in range(right, left - 1, -1):
matrix[bottom][col] = val
val += 1
bottom -= 1
if left <= right:
for row in range(bottom, top - 1, -1):
matrix[row][left] = val
val += 1
left += 1
return matrix
3.3 变体三:从外到内的访问顺序调整
有时候面试官会问,如果不想顺时针,想逆时针怎么办?或者不从外部开始,而是从内部某个点开始螺旋怎么办?
这些追问其实都逃不开核心——边界收缩法和方向数组法的相互转化。当你理解了四个边界的含义后,逆时针无非就是改变四个方向的遍历顺序:先从上到下走左边界,再从左到右走下边界,从下到上走右边界,从右到左走上边界。方向数组法也同样,把 dirs 的顺序改成逆时针向量即可。
所以我的建议是,两种方法都动手实现一遍,尤其是方向数组法,在某些变体中写起来更灵活。你掌握的不只是一个题,而是一整套“在二维矩阵里按规则走”的思维方式。
4. 手写代码时的常见 Bug:这些都是我蹲过的坑
说句实话,这道题第一次写,想一遍通过是有难度的。即使在本地跑通了,有时换一个非方形矩阵,比如 3×4 或 4×3,就容易翻车。下面是我刷题过程中真实遇到过的几个坑。
4.1 遍历下边界和左边界时忽略了边界有效性判断
拿 3×4 的矩阵举例:
text复制[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9,10,11,12]]
第一步遍历上边界,得 [1,2,3,4],top 变为 1。
第二步遍历右边界,得 [8,12],right 变为 2。
第三步从右到左遍历下边界,因为此时 top=1 <= bottom=2,条件满足,得 [11,10,9],bottom 变为 1。
第四步从下到上遍历左边界,因为此时 left=0 <= right=2,条件满足,得 [5],left 变为 1。
此时 top=1, bottom=1, left=1, right=2,进入第二轮。
第二轮把中间行 [6,7] 取完,结束。整个结果是 [1,2,3,4,8,12,11,10,9,5,6,7],完全正确。
但如果我不加 if top <= bottom 判断,在第二步结束后立刻执行第三步,因为 right=2,left=0,矩阵的下边界元素明明已经在第二轮才会被扫描,是否就会出问题?我们来推演一下极端情况:如果矩阵是 2×3 呢?
text复制[[1, 2, 3],
[4, 5, 6]]
初始 top=0, bottom=1, left=0, right=2。
第一步: [1,2,3],top=1。
第二步: [6],right=1。
此时 top=1, bottom=1, left=0, right=1,条件 top <= bottom 为 true,可以执行第三步。第三步从右到左遍历 bottom 这一行,得 [5,4],bottom=0。
第四步条件 left <= right 即 0 <= 1 为 true,执行从下到上遍历左边界。但此时 bottom=0, top=1,range(bottom, top - 1, -1) 就是 range(0, 0, -1),结果为 [0],会取出 matrix[0][0] 也就是 1,但 1 已经被第一步取过了。这就是重复元素来源。
所以这两个 if 判断缺一不可,前者防止“下边界已经收缩到上边界上方”时仍然去读取,后者防止“左边界已经收缩到右边界右侧”时仍然去读取。
4.2 循环边界写成 range(left, right) 而不是 range(left, right+1)
这个坑听起来很基础,但在紧张状态下特别容易犯。因为人脑倾向于把 range 的右端点当作“包含”,写顺手了就容易漏掉最后一个元素。
当你遍历上边界时,列的范围应该是 left 到 right(闭区间),而 Python 的 range 是左闭右开,所以必须写成 range(left, right + 1)。同理,从右到左时是 range(right, left - 1, -1),因为右边的 left - 1 是开区间端点。
我在给朋友 review 代码时就见过他写成 range(left, right),结果每行漏掉最右边的元素,最后整个结果错乱,调试了半天才找到原因。
4.3 对“空矩阵”和“非矩形矩阵”的防御
LeetCode 的测试用例里,矩阵可能是 [] 也可能 [[]]。if not matrix or not matrix[0] 这行防御性判断是非常必要的,它可以同时过滤掉这两种情况。
另外,LeetCode 上的“矩阵”默认都是矩形的,也就是每一行长度相同,但如果你刷的是 ACM 风格或其他平台的题目,有可能出现“锯齿形数组”,即 matrix[0] 的长度不等于 matrix[1] 的长度。这时候 len(matrix[0]) 只代表第一行的长度,按列遍历就会越界。如果你在面试中遇到这种输入格式,需要先跟面试官确认输入约束。
4.4 多语言实现的转换小坑
如果你在面试用 C++ 或 Java 写,需要额外注意数组索引从 0 开始,这与 Python 一致,但 for 循环的写法容易把边界写错。
以 Java 为例:
java复制class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
if (matrix == null || matrix.length == 0) return res;
int top = 0, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (int col = left; col <= right; col++) {
res.add(matrix[top][col]);
}
top++;
for (int row = top; row <= bottom; row++) {
res.add(matrix[row][right]);
}
right--;
if (top <= bottom) {
for (int col = right; col >= left; col--) {
res.add(matrix[bottom][col]);
}
bottom--;
}
if (left <= right) {
for (int row = bottom; row >= top; row--) {
res.add(matrix[row][left]);
}
left++;
}
}
return res;
}
}
Java 的 for 循环内置边界判断,看起来比 Python 更直观,但缩进嵌套更容易看花眼。每次写完我习惯在草稿纸上用一个 4×4 矩阵手动走一遍循环,标注出每个变量的变化,这个方法虽然笨,但极其有效。
5. 从 LeetCode 54 到 hot100 的整体视角:这道题的战略地位
既然标题是 hot100 的第 54 题,那我觉得有必要把它放到整个 hot100 的视角里来看一下。很多同学刷题喜欢按题号顺序盲刷,但我更建议按专题分类刷。hot100 里,除了 54,还有 48(旋转图像)、73(矩阵置零)、240(搜索二维矩阵 II)等高频矩阵题,它们连在一起刷,效率最高。
我自己常用的一个专题组合是:
- 54 螺旋矩阵 + 59 螺旋矩阵 II:解决“按层模拟”的问题
- 48 旋转图像 + 54 螺旋矩阵:解决“矩阵边界操作”的问题
- 240 搜索二维矩阵 II:解决“矩阵中查找”的问题,思路完全不同但很经典
- 73 矩阵置零:考的是状态压缩的空间优化
这些题串起来,你就能形成一个“矩阵题矩阵”,而不是一堆孤立的题。遇到新题时,至少能快速判断出它属于哪一种模式。
另外,我看到热词里有“LeetCode 073 爱吃香蕉的狒狒”和“LeetCode 5: 最长回文子串”,这些都是 hot100 或力扣周赛里常见的搜索词。那“爱吃香蕉的狒狒”其实是经典的二分查找题“Koko Eating Bananas”,和螺旋矩阵正好是两类完全不同的题型。一个是模拟、一个是二分答案,两者放在一起恰好能提醒我们:刷题不能只刷一种类型,思维方式的多样性才是面对新题时的最大底气。
5.1 周赛和 hot100 的关系
热词里还有“LeetCode周赛430”,说明很多读者会关注周赛成绩。周赛的题目往往比 hot100 的沉淀经典题更新、更难,但你会发现,周赛题里经常藏着 hot100 的影子,特别是矩阵相关的题目,剥开外壳后经常是螺旋遍历或方向数组的变体。
所以我的观点是:hot100 是你的基本功,周赛你是拓展上限,两者要并行。如果周赛总是前两题做完就卡住,大概率是基本功还不够熟,与其刷偏题怪题,不如回头把 54 这种经典题练到闭着眼都能写。
5.2 面试中的时间分配与代码习惯
如果这道题作为面试手写题出现,我建议按下面的时间节奏来:
- 前两分钟,和面试官确认矩阵的输入范围和空值情况。
- 中间五到八分钟,直接在白板/编辑器里写代码,不需要多解释,因为这是一道模拟题,按直觉实现即可。
- 写完代码后,用 1×1、1×3、3×1、2×3、3×3、3×4 这六种典型输入快速自查一遍。
尤其不要忽视 1×1 这种极端用例。很多人觉得太简单,顺手在脑子里过一下就跳过了,但恰恰是这种简单用例最容易暴露出“边界范围多加一”的问题。
6. 完整复现代码与自查清单:直接抄作业
最后,我把完整可运行的 Python 代码和一些自查建议放在这里,方便大家直接拿去用。
python复制from typing import List
def spiralOrder(matrix: List[List[int]]) -> List[int]:
"""按顺时针螺旋顺序返回矩阵中的所有元素。"""
if not matrix or not matrix[0]:
return []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
res = []
while top <= bottom and left <= right:
# 上边界:从左到右
for col in range(left, right + 1):
res.append(matrix[top][col])
top += 1
# 右边界:从上到下
for row in range(top, bottom + 1):
res.append(matrix[row][right])
right -= 1
# 下边界:从右到左(注意防越界)
if top <= bottom:
for col in range(right, left - 1, -1):
res.append(matrix[bottom][col])
bottom -= 1
# 左边界:从下到上(注意防越界)
if left <= right:
for row in range(bottom, top - 1, -1):
res.append(matrix[row][left])
left += 1
return res
# 测试用例
if __name__ == "__main__":
test1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(spiralOrder(test1))
# 期望输出: [1, 2, 3, 6, 9, 8, 7, 4, 5]
test2 = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
print(spiralOrder(test2))
# 期望输出: [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
test3 = [[1]]
print(spiralOrder(test3))
# 期望输出: [1]
test4 = [[1, 2], [3, 4]]
print(spiralOrder(test4))
# 期望输出: [1, 2, 4, 3]
在面试或自测时,我建议你把下面这份“自查清单”刻在脑子里,每写完一版都快速核对一遍:
- 空矩阵处理:
not matrix or not matrix[0]有没有写? - 循环条件:是
while top <= bottom and left <= right而不是<? - 第三步和第四步有没有加
if判断? - 遍历方向顺序是不是“上 → 右 → 下 → 左”?
- 用 1×1、1×3、3×1、2×3、3×3、3×4 等不同尺寸的矩阵各测一遍?
这道题刷完之后,我个人还喜欢做一件事:把 if top <= bottom 和 if left <= right 临时删掉,跑几个特殊用例,看看输出错在哪里。这个“故意制造 bug”的过程能让你对边界收缩过程有肌肉记忆,下次再写时,你会在不自觉中避开这些坑。我就是这样把 54 题练到闭着眼也能写对的。
刷题这件事,很多时候不是比谁更聪明,而是比谁见过的坑更多、记忆更深。螺旋矩阵不是一个难的算法题,但它对代码严谨度的要求,足以在一场面试中暴露一个候选人的真实代码功底。希望这篇拆解,能帮你少走一点弯路。
