
螺旋矩阵这道题我在面试候选人的时候几乎必问。它看起来只是“按顺时针方向一圈圈把矩阵里的元素读出来”这么简单但从实际作答情况来看能一次写对边界条件的人真的不多。它考察的东西非常集中二维数组坐标的理解、循环不变量的把握、以及对边界情况的敏感度。不管你是准备算法面试还是做图像处理、矩阵运算相关的工作或者纯粹想练编程基本功把螺旋矩阵吃透都有实打实的价值。这篇文章我会把螺旋矩阵从核心思路到代码实现、从常见变体到踩坑经验一次性讲清楚。你不需要有很强的算法基础只要会二维数组的基本操作就行。我会用尽量直白的话解释每一步为什么要这么做代码部分用 Python 给出可以直接照着抄的实现。1. 螺旋矩阵的核心思路剥洋葱还是画圈1.1 螺旋遍历的本质是坐标的顺序变化先看一个最直观的例子。假设有一个 4×4 的矩阵1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16按顺时针螺旋顺序读取结果是1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10发现规律了吗整个过程就是“右 → 下 → 左 → 上”这样四条边按顺序循环每走完一条边可遍历的范围就缩小一圈。我习惯把这种方式叫“画圈”也有人叫“剥洋葱”——每一轮剥掉最外面的一层然后继续处理里面那一层。这里的核心不是记住读取顺序而是理解坐标变化。矩阵中每个元素的位置由(row, col)决定向右走是col 1向下走是row 1向左走是col - 1向上走是row - 1。螺旋遍历本质上就是这四个方向的周期性重复每次方向切换的时机是“当前方向走到底”的时候。1.2 边界收敛四个变量控制整个流程要控制“走到底”这件事最经典的做法是用四个变量记录当前未遍历区域的边界top当前未遍历区域最上方的行号初始为 0bottom当前未遍历区域最下方的行号初始为m - 1left当前未遍历区域最左侧的列号初始为 0right当前未遍历区域最右侧的列号初始为n - 1每一轮按照“上边从左到右、右边从上到下、下边从右到左、左边从下到上”的顺序走完四条边然后收缩边界上边走完top右边走完right--下边走完bottom--左边走完left循环继续的条件是left right且top bottom。一旦条件不满足说明所有元素都已经遍历完可以退出了。这里有个特别容易忽略的细节向左走完下边、向上走完左边之后必须再次检查循环条件。为什么因为当矩阵只有一行或只有一列时某些边会重复。举个例子3×1 的矩阵只有一列上边和下边是同一条竖线如果不重新检查条件就会把同一列的元素读两次。1.3 复杂度分析为什么螺旋矩阵适合练手从复杂度角度看螺旋矩阵非常“干净”每个元素恰好被访问一次所以时间复杂度和矩阵元素总数成正比也就是 O(m×n)其中 m 是行数、n 是列数。空间复杂度方面如果只需要返回遍历结果额外空间是 O(1)如果需要额外标记是否访问过那就是 O(m×n)。这个“常数极低、逻辑密度高”的特性让它成为算法入门阶段性价比极高的练习题目。2. 两类主流实现方式模拟法与分层法2.1 模拟法用方向数组老实走路第一种思路是模拟法字面意思就是“真的按照螺旋路径一步一步走”。定义一个方向数组directions [(0, 1), (1, 0), (0, -1), (-1, 0)]分别对应右、下、左、上四个方向。每一步都尝试沿着当前方向走如果下一步超出了矩阵范围或者走到了已经访问过的位置就切换到下一个方向。实现时有两种做法一种是用额外的visited矩阵标记访问过的元素另一种是不用额外矩阵直接用边界判断“是否越界”。用visited会更直观代价是多一块空间。模拟法的优点是思路非常贴合人的直觉也容易扩展到不规则形状的遍历。比如以后你想遍历一个蛇形区域或者任意形状的连通区域这种“带着方向走”的思路都能复用。缺点就是代码稍微长一点而且需要额外处理方向切换的时机。2.2 分层法四个边界直接收缩第二种思路是分层法也是我推荐优先掌握的方法。它不关心每一步怎么走而是直接按“层”处理最外层一圈、第二圈、第三圈……每一圈都按照固定的四条边顺序读取读完就收缩边界。用伪代码概括就是初始化 top0, bottomm-1, left0, rightn-1 while top bottom and left right: 1. 从 (top, left) 走到 (top, right) # 上边 2. 从 (top1, right) 走到 (bottom, right) # 右边 3. 如果 top bottom: 从 (bottom, right-1) 走到 (bottom, left) # 下边 4. 如果 left right: 从 (bottom-1, left) 走到 (top1, left) # 左边 收缩 top, bottom, left, right分层法的优点是没有额外空间所有逻辑都集中在边界的收缩上代码非常紧凑。缺点是一开始理解“为什么走完下边和左边还要加条件判断”需要一点时间但一旦想通了写起来反而不容易出错。2.3 怎么选一个对比维度模拟法分层法直觉程度高符合真实走路过程中等需要理解分层模型额外空间用 visited 时为 O(m×n)O(1)代码量稍长更短出错点方向切换时机边界收缩后的重复判断适用场景不规则遍历、迷宫路径类问题标准矩阵/矩形区域的螺旋遍历我的建议是两种都要会写。分层法用来应付面试和竞赛是够的模拟法的思路在更复杂的二维遍历问题里经常能救命。大多数人一开始会觉得模拟法更好理解练几道题之后就会转向分层法因为代码更简洁、变量更少、边界更集中。3. 实操Python 完整实现与关键步骤拆解3.1 先写螺旋读取LeetCode 54 的解法题目是这样的给你一个 m 行 n 列的矩阵按顺时针螺旋顺序返回矩阵中的所有元素。下面是我实际提交时用的代码带注释def spiral_order(matrix): if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) top, bottom 0, m - 1 left, right 0, n - 1 result [] while top bottom and left right: # 第一段上边从左到右 for col in range(left, right 1): result.append(matrix[top][col]) top 1 # 第二段右边从上到下 for row in range(top, bottom 1): result.append(matrix[row][right]) right - 1 # 第三段下边从右到左 # 这里必须判断 top bottom防止单行矩阵重复读取 if top bottom: for col in range(right, left - 1, -1): result.append(matrix[bottom][col]) bottom - 1 # 第四段左边从下到上 # 这里必须判断 left right防止单列矩阵重复读取 if left right: for row in range(bottom, top - 1, -1): result.append(matrix[row][left]) left 1 return result这段代码的核心难点就在第三段和第四段的两个if判断。我第一次写的时候就是没加这两个判断结果在 3×1、1×3 这种很“瘦”的矩阵上反复出现重复元素。调试的时候打印出来才发现当矩阵只剩一行时“下边”和上边是重合的如果不阻止第三段执行就会把这一行倒着再读一遍。3.2 反向操作生成螺旋矩阵LeetCode 59螺旋读取的反向题同样很常见给定正整数 n生成一个包含 1 到 n² 的 n×n 正方形矩阵元素按顺时针螺旋顺序排列。这种题在笔试里出现频率很高实现思路一模一样只是从“读元素”变成“填元素”。def generate_matrix(n): matrix [[0] * n for _ in range(n)] top, bottom 0, n - 1 left, right 0, n - 1 num 1 target n * n while num target: # 上边从左到右填 for col in range(left, right 1): matrix[top][col] num num 1 top 1 # 右边从上到下填 for row in range(top, bottom 1): matrix[row][right] num num 1 right - 1 # 下边从右到左填 for col in range(right, left - 1, -1): matrix[bottom][col] num num 1 bottom - 1 # 左边从下到上填 for row in range(bottom, top - 1, -1): matrix[row][left] num num 1 left 1 return matrix两个实现几乎是一一对应的。区别只有一个方向一个是“从矩阵里取数据”另一个是“往矩阵里放数据”。所以我把两道题放在一起练一次搞定两个思路。填数版本有一个天然优势不用额外判断单行单列情况因为 n×n 矩阵在每一轮循环中至少能走完一整圈只要num target就能保证不会重复填同一格。3.3 关键细节为什么奇偶矩阵都要单独考虑正方形矩阵的边长如果是奇数最中心会剩下一个单独元素。比如 3×3 的矩阵中心位置是 (1, 1)在分层法走完最外圈后top、bottom、left、right会同时指向这个位置。这时候循环条件仍然成立但上边、右边、下边、左边四条边里实际上只有“上边”这个循环能执行——因为top bottom且left right时另外三段会因为条件判断而跳过。最终效果就是正确地把中心元素填上了。很多人在奇数矩阵上报错不是逻辑错了而是把“下边”和“左边”的循环写成了无条件的for导致中心元素被反复填写。这里再次印证了循环不变量的重要性你要保证每个元素只被操作一次。4. 常见变体与应用场景矩阵问题的真正威力4.1 高频变体逆时针、蛇形、从中心向外螺旋矩阵在面试里很少单独出现往往会变着花样考。最常见的几个变体逆时针螺旋方向顺序换成“下 → 右 → 上 → 左”代码逻辑完全同构只要调整方向数组或四条边的执行顺序。蛇形/Z字形矩阵按对角线方向交替往返填充这个更接近模拟法的思路核心是判断当前方向是“斜上”还是“斜下”。从中心向外螺旋常见于图像处理中的螺旋采样或者某些加密算法中的乱序。这种变体需要换一套坐标生成逻辑从中心点开始按照“右、下、左、上、右……”的步长模式逐步扩展。如果你把螺旋矩阵练熟了上面这些变体花不了多少时间就能迁移过去。核心还是坐标系的理解和边界控制。4.2 螺旋遍历在真实项目中的应用螺旋遍历不只是面试题。在图像处理和计算机视觉里有一种常见的操作叫“螺旋采样”就是从图像中心向外做螺旋扫描用于局部特征提取或模糊半径搜索。相机标定里经常提到的 H 矩阵单应矩阵虽然做的是坐标投影变换但你在理解它的时候同样要建立“像素坐标如何按矩阵方式变换”的空间感。另外很多人在做深度学习分类任务时会接触混淆矩阵做线性代数时会接触分块矩阵求逆、矩阵论。这些看似高级的内容落到代码层面时依然逃不开“如何高效、正确地遍历和操作二维数据”这个基本功。矩阵论和分块矩阵的物理意义是数学层面的但如果你能熟练地控制二维数组的遍历学习这些概念时会更快地把公式落成代码。4.3 面试官到底在看什么作为面试过不少候选人的过来人我可以告诉你面试官考螺旋矩阵看的根本不是“会不会背模板”。他们真正在意的是三件事第一你能不能清晰地描述遍历顺序第二你能不能主动说出单行单列、奇数矩阵这些边界情况第三你的代码里有没有重复的、容易出错的分支。如果你能在写代码之前先说出“我打算用四个边界变量每走完一条边收缩一次循环条件是左边界不超过右边界、上边界不超过下边界”那这道题基本就稳了。相反如果你一上来就埋头写for循环写完才发现数组越界这在面试官眼里是减分项。5. 常见问题与排查技巧实录5.1 死循环、重复元素和数组越界我整理了几条高频报错和排查思路症状可能原因排查方法结果缺少元素某条边没走完比如range(left, right)少了一个1打印每一步的坐标看缺口在哪条边结果有重复元素走完下边/左边后没有重新检查top bottom或left right用 1×3、3×1 矩阵单独测试数组越界边界收缩时把top写成了top--或循环条件用了但边界更新放在循环尾部单步调试观察 top/bottom 的实时变化正方形矩阵中心缺失循环条件写成了while top bottom排除了孤立中心点改成小于等于排查这类问题的通用手段是“小矩阵手推 打印坐标”。我一般会先在纸上画一个 3×3 的方格手动写出期望的遍历坐标序列再对照代码里result的实际顺序。绝大多数逻辑错误在第三步就能暴露。5.2 我的调试技巧在循环里打印关键变量这里分享一个我在实际调试中反复用到的技巧。在写螺旋矩阵这类问题的时候我会临时在循环体里加一行打印print(ftop{top} bottom{bottom} left{left} right{right})然后跑一个 2×2 或 3×3 的测试用例观察每一轮循环开始前边界值的变化。如果边界的收缩顺序和我在纸上推的不一致基本就是边界更新的位置放错了。这个方法看起来简单但比单步断点快得多也更容易看出规律。5.3 布尔开关法另一种排查思路如果你实在找不到问题所在还有一个“土办法”用一个同样大小的布尔矩阵记录每个位置是否已经被访问每次写入时检查。它的作用不是优化性能而是让你快速看清哪个格子被重复访问、哪个格子被漏掉了。这个调试思路在指针类题目里也很有用。我在教初学者的时候会建议他们先用“布尔开关法”写一版确保理解正确然后再改成纯边界控制版本。很多人在第一版能跑通的情况下第二版莫名其妙越界其实就是对边界收缩的顺序还没吃透。多练几遍等你能不看题解直接写出没有visited的版本这道题才算真正过关。写在最后的一些经验我个人在实际操作中最大的体会是螺旋矩阵这类题目千万不要盯着代码硬记。拿一张方格纸从 3×3 开始一步步写上期望坐标再对照代码看每一步的边界收缩这才是最快的上手方式。尤其是“走完下边和左边之后要重新判断循环条件”这一点靠背是记不住的只有自己踩过一次重复元素的坑才会真正记住。另外我会建议你把螺旋矩阵和二分查找的边界题放在一起练。它们表面上毫不相干但内核惊人地相似都是在处理“区间收缩”和“循环不变量”。你把这两个问题练透之后再往后学滑动窗口、双指针、甚至分块矩阵求逆和矩阵论里的抽象概念都会有一种“底层逻辑早就见过”的熟悉感。这也算是我这些年做技术面试和写代码下来最想分享的一句话算法题的价值从来不在题型本身而在于它逼你把边界想清楚的能力。