刷LeetCode热题100的朋友几乎没人能绕开螺旋矩阵这道题。题号54题目描述连一行都不到给你一个 m 行 n 列的矩阵按顺时针螺旋顺序返回矩阵中的所有元素。规则一句话就能说清可真到面试现场手写代码翻车率却高得惊人。我最早刷这道题时也栽过跟头代码跑起来要么死循环要么重复收集元素最后对着控制台一点点打日志才反应过来是边界条件写拧了。后来我在不同公司面试里遇到过三次这道题帮别人review代码也看过不少版本慢慢把这道题的坑都摸透了。这道题难不在算法思想而在“模拟一个过程”时的边界管理能力。热题100把它放在中间位置不是没有道理的——它考察的是那种“看着简单、写着易错”的代码功底而这种功底恰恰是很多高强度业务开发里真正需要的东西。1. 为什么“螺旋矩阵”能成为热题100的常驻嘉宾1.1 题目本身到底在考什么先看题目最原始的样子。给定一个矩阵1 2 3 4 5 6 7 8 9 10 11 12按顺时针螺旋顺序输出结果应该是1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7圈数不多但每一圈都要经历“向右、向下、向左、向上”四个方向。难点在于当矩阵不是正方形时最后一圈可能只剩一行或者只剩一列这个时候还是机械地走四个方向就会出现越界或者重复收集。大部分第一次写这道题的人脑子里第一个念头是——我用一个方向数组碰到边界就转向不就行了。这种“方向模拟法”确实是一种思路但它有个隐藏成本你得额外开一个同尺寸的visited数组来标记哪些格子已经访问过。空间复杂度从O(1)变成O(m*n)在面试现场如果被追问能否优化空间很多人就卡住了。1.2 为什么大厂面试喜欢拿它当“手写代码”环节热题100里的题很多有些考动态规划有些考二分有些考图遍历。螺旋矩阵属于“模拟题”这个类别它没有高深的算法套路纯靠逻辑严谨性。面试官选它往往不是为了考你背没背过题解而是想观察你在一个明确规则下能不能把过程拆解清楚能不能想到边界情况能不能在手写代码时保持变量关系清晰。面试反馈里经常看到这样的评价候选人思路清晰代码写对了但花了二十分钟。真实原因就是边界条件反复试错。这种题如果能在十分钟内一遍写对至少说明你有比较强的“状态管理”意识——每一圈走完哪些边界要收缩下一步从哪里继续心里有数。1.3 它和后续题目的关联别小看这道题。它的思想会延展到好几道题上螺旋矩阵II是反向构造给你一个n让你生成螺旋矩阵螺旋矩阵III是从任意起点开始螺旋走还有矩阵旋转、蛇形遍历、之字形打印本质上都是同一类“按规则遍历矩阵”的问题。把螺旋矩阵的边界收缩逻辑吃透后面遇到这些题会轻松很多。2. 先想清楚遍历规律顺时针螺旋的拆解2.1 用“走迷宫”的视角理解螺旋想象你在一个矩形迷宫入口规则很简单沿着当前方向一直走走到墙就右转。这个“墙”有两种一种是矩阵本身的物理边界另一种是你已经踩过的格子。如果不用额外数组怎么判断已踩过的格子答案是想办法让“墙”随遍历进度向内收缩。每一圈由四条边组成上边从左往右走走完这一行后上边界下移一行右边从上往下走走完这一列后右边界左移一列下边从右往左走走完这一行后下边界上移一行左边从下往上走走完这一列后左边界右移一列。这个过程像不像一个矩形框在逐渐缩小没错这就是“边界收缩法”的核心形象。四个变量——top、bottom、left、right——框出当前还没有遍历的区域每走完一条边就把对应的边界往里缩一格。2.2 两种主流思路对比方向模拟法和边界收缩法都能做对但风格差别很大面试表现也不一样。对比维度方向模拟法边界收缩法核心思路用方向数组控制前进方向访问过就转向用四个边界变量框定未遍历区域走完一条边收缩一次额外空间需要visited数组O(m*n)不需要额外数组O(1)代码量略短但转向条件多稍长但逻辑直观出错点转向时机、越界判断单行单列时的重复收集面试官观感能跑通但追问空间优化会露怯边界清晰容易讲明白如果只是为了AC两种方法都行。但面试场景下我强烈推荐边界收缩法。理由很简单它把“访问过”这个隐性状态转化成了“边界变量”这个显性状态每一步都看得见摸得着不会出现“我明明转向了为啥又回去了”这种玄学问题。2.3 特别提醒不要一开始就陷入“逐格模拟”的细节有个常见误区是拿到题就想着“我怎么知道下一步该往哪走”于是开始设计方向数组、设计转向条件。这样也能做但容易漏边界。我的建议是先画一个3×3矩阵从外到里标出访问顺序然后观察每一圈的规律再把规律转成代码。螺旋遍历的规律是“每圈走四条边走完边界收缩”这比“遇到墙就右转”更容易写出正确代码。3. 边界收缩法的完整推导与代码实现3.1 核心不变量每一步都在“未遍历区域”的边界上写边界收缩法之前先明确一个不变量变量top、bottom、left、right分别表示当前尚未遍历区域的上下左右边界遍历过程就是不断从外圈向内圈收缩。用3×3矩阵举例1 2 3 4 5 6 7 8 9初始状态top0, bottom2, left0, right2。第一圈上边访问matrix[0][0], matrix[0][1], matrix[0][2]top变为1右边访问matrix[1][2], matrix[2][2]right变为1下边访问matrix[2][1], matrix[2][0]bottom变为1左边访问matrix[1][0]left变为1。此时top1, bottom1, left1, right1还剩中间的5。继续进入第二轮循环上边访问matrix[1][1]top变成2循环结束。这个例子很清楚地展示了每一圈结束后未遍历区域缩小了一圈而每个元素都被恰好访问一次。3.2 Python实现最推荐的“手写版”Python代码简单直观适合作为面试手写的主语言也适合快速验证思路def spiralOrder(matrix): # 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 res [] while top bottom and left right: # 1. 上边从左到右 for j in range(left, right 1): res.append(matrix[top][j]) top 1 # 2. 右边从上到下 for i in range(top, bottom 1): res.append(matrix[i][right]) right - 1 # 3. 下边从右到左前提是还有行 if top bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom - 1 # 4. 左边从下到上前提是还有列 if left right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left 1 return res这段代码里最关键的是第3步和第4步前面的两个if判断。很多网上的题解版本会把这两个判断省略那是因为它们在while循环条件上做了额外处理但初学者照着写很容易在单行或单列矩阵上报错。加上这两个if逻辑才是完备的。3.3 Java与C的实现差异如果面试官指定Java或C思路完全一样只是语法稍有不同。Java版本核心循环public ListInteger spiralOrder(int[][] matrix) { ListInteger res new ArrayList(); if (matrix null || matrix.length 0 || matrix[0].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 j left; j right; j) { res.add(matrix[top][j]); } top; for (int i top; i bottom; i) { res.add(matrix[i][right]); } right--; if (top bottom) { for (int j right; j left; j--) { res.add(matrix[bottom][j]); } bottom--; } if (left right) { for (int i bottom; i top; i--) { res.add(matrix[i][left]); } left; } } return res; }C版本几乎一样唯一的坑是二维vector的判空比数组严格。如果你是C选手我建议把if(matrix.empty() || matrix[0].empty())写在最前面否则后面对matrix[0]的访问可能直接越界。3.4 复杂度分析为什么说它空间是O(1)时间复杂度矩阵有m×n个元素每个元素被访问一次、加入结果一次所以是O(m×n)。空间复杂度除了返回结果数组res算法本身只用top、bottom、left、right四个变量以及循环里的临时变量。严格来说结果数组是题目要求返回的不计入额外空间。所以额外空间是O(1)。这也是边界收缩法比方向模拟法更优秀的地方同等时间复杂度下空间少了一个量级。面试时把这个复杂度分析讲清楚面试官会认为你真的理解了这道题而不是背了题解。4. 最容易栽跟头的三个细节单行、单列与死循环4.1 单行矩阵为什么会越界也能跑但结果错误考虑一个只有一行的矩阵[1, 2, 3, 4]初始状态top0, bottom0, left0, right3。走完第一步“上边”收集了1、2、3、4top变成了1。此时while条件top bottom是1 0直接退出循环结果正确。看起来没问题对吧但如果去掉代码里的两个if判断流程会怎样第二步“右边”执行的是for i in range(top, bottom1)也就是range(1, 1)空循环不会执行。第三步“下边”执行for j in range(right, left-1, -1)也就是从3到0会把矩阵[bottom][j]也就是matrix[0][3]到matrix[0][0]全部重新收集一遍结果变成重复收集。这就要出大问题了。换句话说单行矩阵的问题不在越界而在第三步不该执行时却被执行了形成了反向重复遍历。这个bug特别隐蔽因为小规模测试时可能看不出问题只有矩阵行数为1或列数为1时才暴露。4.2 单列矩阵的镜像问题对称地单列矩阵[1, 2, 3]^T初始top0, bottom2, left0, right0。第一步上边收集1top变成1。第二步右边收集2、3right变成-1。此时如果不加if left right判断直接执行第四步“左边”会从bottom到top反向再收集一遍结果也是重复。这两种情况本质上是同一个问题在只剩一行或只剩一列的最后一圈不能走完四条边只能走两条边甚至一条边。两个if判断就是在给“是否还有剩余行/列”把关。4.3 while循环条件为什么是而不是这是一个容易被忽略但极其重要的细节。用表示只要当前还有至少一行一列未被遍历循环就继续。如果用当最后只剩一行时top和bottom相等但不会进入循环中间那行元素就会丢失。我见过有人为了避免单行单列重复的问题把while条件改成top bottom left right结果在3×3矩阵上就丢了中心的5。因为3×3矩阵第二轮开始时top1, bottom1top bottom为false循环直接结束中心元素没被收集。这种改法是典型的“顾此失彼”。4.4 死循环的常见诱因与定位技巧死循环在螺旋矩阵里不常见但一旦出现多半是边界变量更新与循环条件不匹配。比如你忘了在某个方向遍历后收缩对应边界top一直不变循环条件永远满足就会反复收集同一个元素。定位死循环的办法很简单在循环里加一个计数器超过m×n次就强制退出count 0 while top bottom and left right: count 1 if count m * n: print(可能死循环了) break # ... 原有的边界遍历逻辑这种调试技巧虽然粗暴但能快速确认问题方向。实际刷题时我建议先用小矩阵手动过一遍3×3、3×4、1×3、3×1各跑一遍能把大部分问题暴露出来。我实战中的一条经验是写完代码后不要急着提交先在草稿纸上用一个3×4或4×3的矩阵走一遍。手写走通一遍基本就不会有边界问题了。很多时候你对着代码发呆找不到问题但手推一遍立刻就能看出来是哪一步的边界写错了。5. 变种题型从螺旋遍历到螺旋构造与螺旋路径5.1 LeetCode 59 螺旋矩阵II思路反过来螺旋矩阵II要求给定正整数n生成一个包含1到n²所有元素的螺旋矩阵。它和54题正好相反54题是遍历读出来59题是按顺序写进去。实现上同样用边界收缩法区别只是把append改成赋值def generateMatrix(n): matrix [[0] * n for _ in range(n)] top, bottom 0, n - 1 left, right 0, n - 1 num 1 while top bottom and left right: for j in range(left, right 1): matrix[top][j] num num 1 top 1 for i in range(top, bottom 1): matrix[i][right] num num 1 right - 1 if top bottom: for j in range(right, left - 1, -1): matrix[bottom][j] num num 1 bottom - 1 if left right: for i in range(bottom, top - 1, -1): matrix[i][left] num num 1 left 1 return matrix在热题100里59题经常和54题成对出现。面试时如果先考54题大概率会追问“能不能反过来生成一个螺旋矩阵”所以这两题建议一起准备。5.2 LeetCode 885 螺旋矩阵III带起点的螺旋885题就更进阶了给你起点坐标(rStart, cStart)和行数列数从起点出发螺旋遍历只记录矩阵范围内的坐标。它和标准螺旋矩阵的差别在于螺旋路径的中心不在矩阵中心甚至可能起点就在矩阵外面。这种题的解法是用方向模拟法加“步长递增”规律向右1步、向下1步、向左2步、向上2步、向右3步、向下3步……每走完两个方向步长加1。这也是“螺旋”二字的本质每一步走的距离是1, 1, 2, 2, 3, 3, 4, 4...这类题在面试中出现频率不如前两者高但在大厂的加面轮次里有可能作为扩展题出现。它的核心启示是螺旋遍历的本质是“步长按规律递增的方向行走”边界收缩法只是它在矩阵场景下的一种简化表达。5.3 常见面试变形逆时针、蛇形、从中心开始逆时针螺旋把方向数组的顺序从“右下左上”改成“下右上左”或者把四条边的遍历顺序换一下。理解边界收缩法后改动成本很低。蛇形遍历之字形打印不按完整螺旋走而是第一行从左到右、第二行从右到左、第三行再从左到右。它比螺旋简单但常见于电话面试快速筛选。从中心开始向外螺旋本质是885题的镜像更适合用方向模拟法实现。面试时如果被问到可以先说自己熟悉的边界收缩法然后分析为什么这类题更适合方向模拟。我建议准备程度是这样的54题必须闭眼能写59题必须能快速改出来885题了解思路即可逆时针和蛇形作为扩展了解。6. 实战复盘我在面试现场讲这道题的过程6.1 拿到题目后第一分钟做什么我第一次在面试里遇到螺旋矩阵时第一反应是有点慌因为太熟了反而怕写太快显得像背题。后来我总结了一套稳妥的节奏先把题目用自己的话复述一遍跟面试官确认输入输出。然后不要急着写说一句“我画个3×3的矩阵先看一下每一圈的访问顺序”。画图这个动作很重要一方面帮自己理清思路另一方面让面试官看到你的思考过程。画完图之后我会说“我准备用四个边界变量来维护当前未遍历区域每走完一条边就收缩对应的边界”。一句话把方案讲清楚然后再动笔写代码。6.2 边写边讲的节奏写代码时不要闷头写每写一段就说一下这段在做什么“这里先遍历上边从左到右走完top加1。” “接下来遍历右边从上到下走完right减1。” “在走下边之前我加了一个if判断。因为如果只剩一行上边走完top已经大于bottom这时不应该再走第三、第四步否则会重复收集。”这些话看起来像是在自言自语其实是在向面试官展示你的边界意识。我见过不少候选人代码写得飞快但面试官问“这里为什么要加if”时答不上来最后反而留下不好的印象。边写边讲能避免这种尴尬。6.3 面试官可能追问的扩展点写完代码并且跑通基本用例后面试官通常会有三个方向的追问。第一个追问“时间复杂度是多少”这个好答每个元素访问一次O(m×n)。第二个追问“空间复杂度呢”如果用的是边界收缩法直接答O(1)并说明除了返回数组只用四个变量。第三个追问“如果矩阵是空的怎么办”这个问题其实在代码开头已经处理了if not matrix or not matrix[0]: return []。但要注意如果是Cmatrix[0]可能为空要对matrix.empty()单独判断。还有一个小概率追问“能不能用递归实现”答案是可以但没必要。递归实现每一圈调用一次本质上还是边界收缩但代码更绕可读性变差。如果被问到我会说“递归在这里没有额外收益反而增加栈开销和代码复杂度迭代实现更直观”。6.4 我复盘后发现这道题的真正价值在于“状态管理”刷完这道题很久之后回头看我越来越觉得螺旋矩阵的真正价值不在于那个螺旋本身而在于它训练了一种“状态管理”的思维。写业务代码的时候你经常要维护多个状态变量比如分页参数、游标位置、当前有效范围。螺旋矩阵的top、bottom、left、right四个变量本质上就是一种“有效范围”的管理。每处理一段数据范围收缩一次直到范围为空。懂得了这个抽象你会发现很多“模拟类”的问题都有类似的解法比如矩阵旋转、数组逆序、双指针收缩本质上都是在维护“还有效的范围”。从这个角度看热题100把它列为必刷题并不是因为它难而是因为它小而有代表性。它用最小的代码量把“边界管理、状态更新、异常分支”这三件事全考了一遍。能把这道题讲透、写对、说清复杂度面试官对你代码能力的判断基本就有底了。最后再分享一个小经验刷这种“模拟过程”的题千万别只看题解。自己动手画矩阵、写代码、跑测试踩一遍坑记忆才深。我每次给朋友讲这道题都会先让他写写错了我再带着他一起看边界效果比直接甩一份标准答案好太多。