近半年来我刷 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 0bottom 2left 0right 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 1bottom 1left 1right 2矩形还有一层继续循环。走 top 行输出 6、7top 变成 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 0bottom 0left 0right 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右边变成 0left 还是 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. 我刷这题时踩过的坑与最终习惯这题我从第一次 WAWrong Answer到后来能闭眼写出来大约经历了三个阶段。第一阶段是“看了解析觉得自己懂了”一写就错大部分错在单行单列上第二阶段是“记住了四边界法模板”但碰到 3×3 这种奇数矩阵时最后一轮循环还是会产生重复输出第三阶段才真正理解“边界收缩的意义是让几何区域逐步消失而不是让循环变量自己瞎跑”。我现在刷这题的习惯是不管用什么语言写先想清楚三件事——当前矩形的四个角坐标是什么、哪些边在这个状态下物理存在、每条边的遍历方向是从哪到哪。然后我会在纸上画一个 3×3 矩阵从第一个元素开始一步步写出轨迹。这个动作大约需要三十秒但却能帮我避免 90% 的边界错误。如果你正在刷题我特别建议你也养成这个“画轨迹”的习惯。不要相信脑内模拟一定要落笔或者用注释写出来。因为螺旋矩阵的坑不在于算法复杂而在于人的工作记忆容量有限一旦循环次数超过三层就很容易漏掉某条边的更新。画出来之后代码反而变得异常简单——它变成了对一个几何过程的忠实翻译。最后分享一个小技巧如果你不确定自己写的四边界法是否正确可以先用while True版本快速验证逻辑再改写成while top bottom and left right版本。我在本地练习时经常用这个思路对比两种写法的输出发现大多数错误都集中在“边界收缩后矩形退化”的那一步。把这些边界情况试完这题才算真正过关。
