
刷力扣热题100的朋友螺旋矩阵这道题基本是绕不开的。它排在数组章节比较靠前的位置题号是54在hot100列表里序号大概是第14所以很多刷题攻略里叫它“螺旋矩阵14”。别看它名字听着玄乎本质就是“按顺序遍历二维数组”而且是完全模拟遍历顺序的那种。难就难在“方向变化”和“边界收缩”这两个点上边界没处理好就很容易死循环、重复读、越界。这篇文章我结合自己刷这道题的经历把两种主流的写法、常见的坑、以及它引申出来的变体题一次讲透。先说这道题适合谁看如果你刚开始刷hot100需要用一道典型的二维数组遍历题来建立“边界感”可以细看如果你已经会做也可以跳着看后面的“面试讲法”和“避坑清单”那部分是刷题之外的加分项。1. 从题目到思路螺旋矩阵到底在考什么1.1 螺旋顺序的直观理解力扣54题的题目描述很短给你一个 m 行 n 列的矩阵 matrix请按照顺时针螺旋顺序返回矩阵中的所有元素。什么叫顺时针螺旋顺序就是先从左上角开始往右走一直走到这一行的最右边然后往下走到矩阵最下面再往左走到这一行的最左边再往上走回到第二行的位置然后继续往右、往下、往左、往上……一圈一圈往里收像剥洋葱一样一层一层把数字全部取出来。举个例子1 2 3 4 5 6 7 8 9 10 11 12这个 3 行 4 列的矩阵螺旋遍历结果是1 → 2 → 3 → 4 → 8 → 12 → 11 → 10 → 9 → 5 → 6 → 7。注意中间那部分也是螺旋的从 5 往右走到 6再往下走已经没路了所以往右到 6 就直接进入下一层7 也是从 5 → 6 → 7 的顺序走完的。很多人第一次写就是在这里乱了。这种“从外到内、逐层收缩”的模式对应到代码上的核心操作就是一个字收。每走完一条边就把那一条边对应的边界往里收一格。1.2 为什么这道题是 hot100 里的常客力扣热题100里收录的题要么是高频面试题要么是某个算法思想的典型代表。螺旋矩阵属于后者它不涉及高深的算法但它非常考验一个基础的编码能力对边界条件的控制。实际面试里面试官问这道题往往不是想考你会不会“递归”“动态规划”而是想看你在一个不太复杂但需要细致处理的问题面前能不能写出逻辑完整、边界正确的代码。能不能把问题拆成“四个方向四个边界”能不能把循环终止条件想清楚比能否秒答更重要。另外这道题还有一个“近亲”题目力扣59题螺旋矩阵II。那道题是反过来给你一个正整数 n要求生成一个 1 到 n² 的螺旋矩阵。你会做54题59题基本就是倒过来写。所以刷一道等于刷两道性价比很高。2. 解法一四边界收缩法最容易讲清楚的做法2.1 维护四个边界的核心逻辑第一种解法也是我认为最好理解、面试时最推荐讲的解法就是维护四个边界值left、right、top、bottom。初始值很好定义left 0指向最左边一列right n - 1指向最右边一列top 0指向最上边一行bottom m - 1指向最下边一行然后进入循环循环里按四个方向依次遍历从 left 到 right遍历 top 这一行遍历完 top上边界往下收一行。从 top 到 bottom遍历 right 这一列遍历完 right--右边界往左收一列。如果 top bottom从 right 到 left遍历 bottom 这一行遍历完 bottom--下边界往上收一行。如果 left right从 bottom 到 top遍历 left 这一列遍历完 left左边界往右收一列。循环条件是 left right top bottom。每次循环走完一圈四个边界都往里缩一格直到矩阵被“剥”完。下面这段是 Java 的参考实现public ListInteger spiralOrder(int[][] matrix) { ListInteger res new ArrayList(); if (matrix null || matrix.length 0) return res; int m matrix.length; int n matrix[0].length; int left 0, right n - 1; int top 0, bottom m - 1; while (left right top bottom) { // 左到右 for (int i left; i right; i) { res.add(matrix[top][i]); } top; // 上到下 for (int i top; i bottom; i) { res.add(matrix[i][right]); } right--; // 右到左需要判断是否还有行 if (top bottom) { for (int i right; i left; i--) { res.add(matrix[bottom][i]); } bottom--; } // 下到上需要判断是否还有列 if (left right) { for (int i bottom; i top; i--) { res.add(matrix[i][left]); } left; } } return res; }核心思想就一句话四个方向走完就缩直到两个边界撞到一起。2.2 边界收缩时最容易踩的三个坑这个解法虽然逻辑简单但第一次写几乎所有人都会踩坑我自己也是调了半天才发现问题。第一个坑右到左和下到上这两个循环必须加 if 判断。为什么因为前两步已经让 top、right-- 了。如果只有一行比如top bottom走完左到右top 已经大于 bottom此时再执行右到左的循环就会重复读取上一行已经读过的元素或者越界。同理如果只有一列必须判断 left right 才能走下到上。这个判断不是防御性编程是逻辑上必须的。你可以试着去掉这两个 if用一个 1 行 4 列的矩阵跑一遍结果一定多出几个重复数字。第二个坑while 循环条件的等号不能丢。必须是left right top bottom不是。因为最后一层可能只剩一行或一列这时候 left 等于 right或者 top 等于 bottom如果用了小于号那一层就不会进入循环结果会漏数据。但这个问题很多人是反着踩的——他们在 if 判断里写了等号又在 while 里写了结果白跑半天。第三个坑矩阵不是方阵。如果矩阵是长方形m 不等于 n螺旋到中间时会出现只剩一行或者只剩一列的情况。这时候第三和第四个方向的 if 判断就特别重要。面试官经常考这种变体比如用matrix [[1,2,3,4],[5,6,7,8]]这种 2 行 4 列的例子让你跑一遍就是为了看你有没有处理好“非方形矩阵”的边界情况。3. 解法二方向向量模拟实现更优雅但有一处关键判断3.1 用方向数组控制走向的细节第二种解法思路不太一样它不显式维护四个边界而是用一个方向数组控制前进方向。核心思路是先往右走走到头了往下走到头了往左走到头了往上周而复始。那“走到头”怎么判断两种方式一种是越界了另一种是下一个位置已经访问过了。这种方式在代码上更接近“模拟人走的路径”相比边界法它更通用尤其适合处理多方向的遍历问题。实现上需要额外用一个 visited 二维数组来标记每个位置是否被访问过。方向数组定义如下// 右、下、左、上 int[][] dirs {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};每次移动时先按当前方向计算出下一个位置 (nextRow, nextCol)如果这个位置越界或者已经被访问过就切换到下一个方向。Java 参考实现public ListInteger spiralOrder(int[][] matrix) { ListInteger res new ArrayList(); if (matrix null || matrix.length 0) return res; int m matrix.length, n matrix[0].length; boolean[][] visited new boolean[m][n]; int dirIndex 0; int row 0, col 0; int total m * n; for (int i 0; i total; i) { res.add(matrix[row][col]); visited[row][col] true; int nextRow row dirs[dirIndex][0]; int nextCol col dirs[dirIndex][1]; if (nextRow 0 || nextRow m || nextCol 0 || nextCol n || visited[nextRow][nextCol]) { dirIndex (dirIndex 1) % 4; nextRow row dirs[dirIndex][0]; nextCol col dirs[dirIndex][1]; } row nextRow; col nextCol; } return res; }这段代码的关键判断只有一处if (越界 || 已访问)只要条件成立就换方向。因为每走一步最多只需要换一次方向所以不需要用 while 循环不停转向。3.2 两种解法的复杂度对比与取舍先做复杂度对比其实两种解法时间复杂度和空间复杂度不一样这是面试中值得主动提的点。解法时间复杂度空间复杂度是否需要 visited 数组四边界收缩法O(m*n)O(1)结果数组不算额外空间不需要方向向量模拟O(m*n)O(m*n)需要如果只从“最优解”角度看四边界收缩法在空间上明显更好所以笔试和面试中大多数答案都偏向方法一。但方向向量模拟也很有价值它的泛化能力更强。比如矩阵遍历路径是“蛇形”“之字形”或者“按某个不规则路径行走”时你只需要改方向数组或者扩展转向条件代码骨架完全不用动。所以我的建议是两个写法都要掌握。考场上优先写边界收缩法因为内存占用低、逻辑直观。但平时练习时把方向向量模拟也写熟遇到路径类变体题时方向数组的解法能帮你节省大量思考时间。这里再补充一点方向向量模拟能不能做到 O(1) 空间可以。有一种做法是直接把访问过的位置的值改成一个特殊值比如 Integer.MIN_VALUE这样下次判断到这个值就知道访问过了。但这样做污染了原数组在面试中需要先跟面试官确认是否允许修改输入数据。比如题目允许的话可以这样做if (nextRow 0 || nextRow m || nextCol 0 || nextCol n || matrix[nextRow][nextCol] Integer.MIN_VALUE) { dirIndex (dirIndex 1) % 4; nextRow row dirs[dirIndex][0]; nextCol col dirs[dirIndex][1]; } matrix[row][col] Integer.MIN_VALUE;但这个方法有个局限如果矩阵元素本来就可能包含 Integer.MIN_VALUE就会产生误判。所以实战中我默认还是用 visited 数组稳妥第一。4. 题变由“读”到“写”的螺旋矩阵II4.1 螺旋填充的思路迁移力扣59题是螺旋矩阵的经典变体题目要求给定正整数 n生成一个包含 1 到 n² 的 n x n 方阵数字按照螺旋顺序排列。换句话说54题是“把螺旋的读出来”59题是“把螺旋的写进去”。逻辑上核心完全一样都是维护四个边界向内收缩只是“读”变成“写”。以 n3 为例1 2 3 8 9 4 7 6 5写代码的时候只需要把边界收缩法的核心逻辑拿过来把res.add(matrix[top][i])改成matrix[top][i] num就行了。但有几个细节值得注意。第一59题一定是 n x n 方阵所以不需要考虑长方形矩阵那种“只剩一行/一列”的复杂情况。当然if 判断还是建议保留避免逻辑漏洞。第二填充顺序一定是“左到右、上到下、右到左、下到上”方向不能乱。很多人写 59 题的时候方向顺序写反了变成逆时针填充结果前几个例子跑对了n 稍微大一点就乱。第三边界收缩的判断和 54 题完全一致每个方向走完就把对应边界往里缩。缩边界的时机一定要在方向结束后立刻执行不要在最后统一处理否则中间的方向计算会错。下面这个 59 题参考代码我用的就是和 54 题几乎一样的骨架public int[][] generateMatrix(int n) { int[][] matrix new int[n][n]; int left 0, right n - 1; int top 0, bottom n - 1; int num 1; while (left right top bottom) { for (int i left; i right; i) { matrix[top][i] num; } top; for (int i top; i bottom; i) { matrix[i][right] num; } right--; if (top bottom) { for (int i right; i left; i--) { matrix[bottom][i] num; } bottom--; } if (left right) { for (int i bottom; i top; i--) { matrix[i][left] num; } left; } } return matrix; }这道题在面试中也比较常见一般作为“你做过54题螺旋矩阵吗好那试试59题”的追问形式出现。本质就是考察你能不能把读的过程逆过来写成写的过程。4.2 面试现场如何把这道题讲出亮点很多人在面试时能做对题但讲不出亮点非常可惜。螺旋矩阵这道题有一句话如果你能主动说出口面试官对你的评价会立刻提升一个档次“这道题本质上是一个模拟问题关键是把变化的边界条件抽象成四个变量每次走完一条边就更新对应的边界循环终止条件就是上边界越过下边界或左边界越过右边界。”这样说就证明了你不只是会写代码还理解了问题本质。如果面试官继续追问“还能不能优化”你可以主动提一下方向数组模拟方法并说明它比边界法多 O(mn) 空间但更容易扩展到其他路径遍历问题。这种“提出另一种解法并准确分析其优缺点”的能力在面试评分中占比很高的。另外还有一个小的加分点。可以提一下“什么时候不会出现螺旋结构”——如果矩阵是空矩阵或者 0 行直接返回空结果提前处理。这种边界条件的敏感度也是面试官会重点观察的。5. 常见问题排查与实操心得5.1 调试螺旋矩阵时最典型的几个问题我在刷题群里见过太多人在螺旋矩阵上卡住反复提交、反复报错。我把最常见的几个问题整理成了一张速查表你可以对号入座。症状根本原因修复方式结果中出现了重复数字右到左或下到上的循环没有先判断 top bottom / left right加上对应 if 判断最后一行的数字没有输出while 条件写成了丢掉了等于的情况改成left right top bottom死循环边界更新漏写了某一句比如没执行 left检查四次边界收缩是否都在对应方向结束后执行越界报错矩阵不是方阵某一方向循环时读取了不存在的行或列用长方形矩阵测试用例自查确认 if 判断覆盖到了输出顺序错乱在内部循环结束后才统一缩边界导致方向判断用错了边界每条边走完立刻缩对应边界其中“死循环”是我见过最阴间的错误。比如边界法里如果右到左的循环写的是for (int i right; i left; i--)但这一步忘记在最外层 while 里做相关边界的正确收缩某些输入下边界就永远不收敛程序卡死。这种问题靠肉眼看很难发现最快的定位方式是在每个方向结束后打印四个边界的值看看是否符合预期。5.2 一个实用的自测清单这道题提交之前我强烈建议你用下面几组测试用例自测覆盖所有典型情况。第一组非空长方形矩阵[[1,2,3,4],[5,6,7,8]]期望结果是[1,2,3,4,8,7,6,5]。这组主要测“只有两行”的情况重点检查右到左的 if 判断有没有问题。第二组单行矩阵[[1,2,3]]期望结果是[1,2,3]。这组主要测“走下边”和“走上边”这两个方向的循环是否被正确跳过。第三组单列矩阵[[1],[2],[3]]期望结果是[1,2,3]。这组主要测“右到左”和“左到右”方向的循环是否正确跳过。第四组空矩阵[]期望结果是空列表。这组主要测开头特判是否生效。第五组1x1 矩阵[[5]]期望结果是[5]。这组测 while 循环条件带等号时的表现。这五组全部跑通代码基本就稳了。我自己每次写这道题无论用哪种写法都必跑这五组用例。虽然看着简单但真的能拦住大量低级错误。另外再说一个实际刷题中的小技巧如果你在 IDE 里调试可以把中间过程打出来看。特别是四个边界法的写法加一行System.out.println(top top , bottom bottom , left left , right right);放在每圈结束时打印你就能清晰地看到边界是如何一步步收缩的。对初学者来说这个可视化的过程比任何讲解都直观。我个人在实际操作中的体会是螺旋矩阵这道题一遍写出无 bug 的代码不是运气而是对边界收缩有肌肉记忆。你不需要背代码你需要理解每一个边界变量代表的意义然后在脑中模拟一遍它逐层收缩的过程。这种能力一旦建立遇到任何“遍历路径有规律”的题目你都会比别人更快进入状态。最后再分享一个小技巧如果你在纸上画一个 4x4 的方阵把螺旋路径用箭头标出来然后对照代码一行行走一遍这是理解这道题最快的方式。不要只在脑子里想动手画一次比看十篇题解都有用。上机刷题是一件需要手感的事螺旋矩阵这道题值得你多花点时间把它写熟练。