
LeetCode Hot 100 第19题螺旋矩阵掌握“边界收缩法”后这类题就是送分在 LeetCode Hot 100 里有一类题特别有意思看起来跟算法没什么关系纯粹考验你“会不会写循环”。螺旋矩阵就是最典型的代表。很多人在面试中碰到它第一反应是“这不就是个二维数组遍历吗”结果一上手就发现要么死循环要么边界越界要么漏掉中间那一圈。我刷完 Hot100 之后回过头看螺旋矩阵其实是在考两件事模拟过程的能力和边界条件的管理能力。这篇文章就以 LeetCode 第 54 题“螺旋矩阵”为例把这类题的完整解法、踩坑点、变形思路一次讲透。这篇文章适合正在刷题准备面试的朋友也适合那种“看题解能看懂、自己写就废”的选手。我会从最核心的解题思路讲起逐步拆解代码实现再补充一些我实际刷题过程中总结的经验和教训。只要你认真读完再遇到螺旋矩阵不管是从外圈到内圈、从内圈到外圈还是逆时针、不定行列数都能顺手写出来。1. 螺旋矩阵到底在考什么螺旋矩阵的题目描述很简单给你一个m行n列的矩阵matrix请按照顺时针螺旋顺序返回矩阵中的所有元素。比如下面这个矩阵1 2 3 4 5 6 7 8 9 10 11 12输出结果应该是[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]。很多人第一眼看到这个题目直觉就是“按层遍历”思路没问题。但真正动笔的时候麻烦就来了for循环里到底是还是每次遍历完一行或一列后边界到底怎么更新break条件写在什么位置才不会死循环1.1 从做菜的角度理解“模拟遍历”我给你打个比方。螺旋矩阵的遍历方式就跟你在家里擦一张圆桌一样——从最外圈开始擦顺时针转圈一圈擦完再擦里面一圈直到桌面中心。你要知道当前擦到哪一圈了每擦完一条边桌子的范围就缩小一点直到最后没地方可擦了。放到代码里“桌子范围”就是四个边界变量上边界top、下边界bottom、左边界left、右边界right。每一轮遍历四条边然后四个边界同时向内收缩一格如此循环直到上下边界交叉或左右边界交叉遍历结束。1.2 为什么要选“边界收缩法”而不是模拟方向变换螺旋矩阵的解法大体上有两类一类是“方向模拟法”用方向数组控制当前位置的走向碰到边界或已访问过的格子就转向另一类是“边界收缩法”每遍历一条边就把对应边界往里收一格。这两类方法我在实际对比后比较推荐边界收缩法。原因有三个第一代码逻辑更直观。方向模拟法需要单独维护一个visited数组或状态标记代码量不小。边界收缩法则不需要额外空间四个变量管到底。第二不容易出错。方向模拟法在边界判断上更绕容易在转向条件上出 bug。边界收缩法的每一次for循环都对应一条实实在在的边路径清晰。第三扩展性好。边界收缩法可以很自然地演化出逆时针遍历、从内向外遍历、螺旋矩阵生成等变形题的解法。你只要把思路吃透做一道题相当于做了四五道题。2. 核心细节解析那四个边界变量怎么管理解边界收缩法关键就一件事每一圈遍历时四条边的起止位置到底怎么确定。我们以顺时针螺旋为例每一圈的遍历顺序是从左到右走上面一行从上到下走右边一列从右到左走下面一行从下到上走左边一列。每走完一条边对应的边界就向内收缩一行或一列。2.1 四条边的遍历规则假设当前矩阵的遍历范围由top、bottom、left、right四个变量界定那么这一圈的四次遍历分别是从左到右遍历matrix[top][left]到matrix[top][right]走完这一行后top从上到下遍历matrix[top][right]到matrix[bottom][right]注意这里的top已经更新过了所以起始行是新的top走完这一列后right--从右到左遍历matrix[bottom][right]到matrix[bottom][left]这里的right也已经更新走完这一行后bottom--从下到上遍历matrix[bottom][left]到matrix[top][left]bottom和top都已更新走完这一列后left。这就是边界收缩的精髓每遍历一条边就立刻修改对应的边界这样后面的边不用重复判断“哪些格子已经走过了”。2.2 循环终止条件为什么是 left right 或 top bottom很多初学者会奇怪为什么要判断left right和top bottom因为螺旋遍历到最内圈可能会出现只剩一行或只剩一列的情况。比如一个 3×4 的矩阵外层走完之后剩下的内部区域可能只有一行如果还是机械地走四条边那么“从右到左遍历下边”和“从上到下遍历右边”就会把已经收集过的元素再收集一遍结果就是结果集里出现重复元素。所以在第二和第三条边的遍历前需要判断上下边界是否还满足top bottom在第三条边开始前需要判断左右边界是否还满足left right。而整个大循环的终止条件就是top bottom || left right代表四周边界已经全部收缩完没有可遍历的区域了。2.3 为什么边界判断顺序不能乱网上很多题解的四条边写法看起来差不多但关键差异就在边界更新和判断的顺序上。如果有人写完发现结果哈希乱多半是这三个位置出了问题走完上边后没有及时top导致右边这列又从原来的顶部开始走走完右边后没有及时right--导致下边这行会走到已被访问的区域第三条边和第四条边之前没有判断边界是否已经越界导致在只剩一行或只剩一列时重复遍历。这三点是螺旋矩阵 code 实现中最高频的 bug 来源。先记下来后面调试时会节省大量时间。3. 实操过程与核心代码实现有了前面的理论基础现在进入实操环节。我用 Python 写一个可以直接跑的完整版本并且用具体例子一步步走一遍确保你不仅会背代码更知道每一行为什么这么写。3.1 完整代码Python 版def spiralOrder(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: # 从左到右遍历上边 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 # 从右到左遍历下边注意检查 top 是否已越过 bottom if top bottom: for col in range(right, left - 1, -1): res.append(matrix[bottom][col]) bottom - 1 # 从下到上遍历左边注意检查 left 是否已越过 right if left right: for row in range(bottom, top - 1, -1): res.append(matrix[row][left]) left 1 return res这份代码有两个关键保护判断第三个for之前的if top bottom以及第四个for之前的if left right。它们保证了即使矩阵只剩一行或一列也不会把已经访问过的元素再遍历一次。3.2 手动推演3×4 矩阵走一遍就拿前面那个 3×4 的矩阵举例1 2 3 4 5 6 7 8 9 10 11 12初始状态top 0, bottom 2, left 0, right 3。第一轮遍历上边输出1, 2, 3, 4然后top变为1遍历右边从第 1 行开始输出8, 12然后right变为2检查top bottom1 2 成立从右到左遍历下边输出11, 10, 9然后bottom变为1检查left right0 2 成立从下到上遍历左边输出5然后left变为1。第一轮输出结果[1, 2, 3, 4, 8, 12, 11, 10, 9, 5]。此时top 1, bottom 1, left 1, right 2矩阵剩下中间一层6 7第二轮遍历上边输出6, 7然后top变为2遍历右边因为top bottomfor row in range(2, 2)不会执行right变为1检查top bottom2 1 不成立第三条边不执行检查left right1 1 成立但for row in range(bottom, top - 1, -1)即range(1, 1, -1)为空什么都不做。第二轮输出结果[6, 7]。此时top 2, bottom 1top bottom大循环结束。最终结果拼接为[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]跟预期完全一致。你注意观察第二轮的右边和下边其实都没有实际元素可取但因为保护判断存在代码不会出错也不会重复。3.3 Java 版参考如果你是用 Java 刷题可以参考下面这个版本逻辑和 Python 完全一致只是语法不同class Solution { 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 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 版的难点主要在于List的初始化方式和数组长度获取方式不太一样。另外注意matrix[0].length要在判断完matrix.length 0之后再取否则空矩阵会直接抛异常。3.4 复杂度分析面试时怎么说螺旋矩阵的时间复杂度是 O(m×n)因为每个元素恰好被访问一次。空间复杂度有两种口径如果把结果集res算进去那需要 O(m×n) 的额外空间如果不算返回结果那么额外空间是 O(1)因为只用了四个边界变量和若干个循环变量。面试官如果追问“能不能优化空间”你可以回答由于题目本身要求返回所有元素结果集的空间是必须的。除此之外我们并没有使用额外的哈希表或访问标记数组所以额外空间已经是 O(1)。这类题目通常不要求原地输出所以 O(1) 额外空间就是最优。4. 常见问题与排查技巧实录螺旋矩阵这道题LeetCode 上提交量很大评论区里常年能看到各种诡异的 bug。我就把我自己刷题时踩过的坑以及身边朋友问得最多的问题整理一下每个都有自己的排查思路。4.1 为什么输出结果有重复元素出现重复元素最典型的原因是没有在第三条边和第四条边之前添加边界判断。当矩阵只剩一行或只剩一列时第三条边从右到左会沿着这一行往回走把已经输出过的元素再输出一遍。第四条边同理。排查方法很简单拿一个 1×3 的矩阵手动走一遍比如[[1, 2, 3]]。不加保护判断时第一轮上边输出1, 2, 3然后第三条边又输出3, 2, 1结果直接出错。加了保护判断后因为top更新为 1bottom还等于 0top bottom不成立第三条边被跳过结果正确输出[1, 2, 3]。4.2 为什么结果少元素甚至中间整圈丢失少元素的常见原因是循环条件写成了while (top bottom left right)。看起来好像也没什么问题可是对于 3×3 这种奇数行列的矩阵遍历到最中间时top bottom且left right此时循环条件不成立正中间那个元素就没被收集。正确的循环条件应该是while (top bottom left right)保证最后一行或最后一列也能被处理。这个点也是面试官常埋的坑我在模拟面试时被问过不止一次。4.3 空矩阵和边界形状的极端用例LeetCode 的测试用例里必然包含[]和[[]]这类空矩阵。如果不先做判空处理matrix[0]就会直接报索引越界。所以代码开头一定要有if not matrix or not matrix[0]: return []另外还有一种比较隐蔽的边界形状是[[1, 2, 3, 4]]1×4只有一行和[[1], [2], [3], [4]]4×1只有一列。前者考验第三条边的保护判断后者考验第四条边的保护判断。这两类用例我强烈建议你自己跑一遍跑完对边界处理的理解会深很多。4.4 什么时候先更新边界什么时候后更新边界这个细节我用一句话总结每条边遍历完边界立刻收缩下一条边的起点一定基于收缩后的新边界。换句话说四个边界的或--操作必须紧跟对应的for循环不能攒到最后一起更新。我见过有人把四个边界更新放到一圈结束之后结果第二圈开始的位置全错了。原因很简单你走完上边后如果不更新top右边这条列的起始行还是 0就把第一行的最后一个元素重复输出了一次。这种错误在逻辑上不太容易一眼发现但输出结果一定不对。4.5 调试螺旋矩阵的通用技巧我的调试习惯是先把矩阵打印或手动写出来在代码里临时加上print(top, bottom, left, right)每走完一条边输出一次这四个变量的变化。这样一来如果某个边界在某轮没有正常向内收缩一眼就能看出来如果第三个for和第四个for的执行条件判断有误也可以通过边界值的变化发现问题配合输出当前收集到的元素可以精确定位是哪条边重复或漏掉了元素。这种“边界值追踪法”比单纯看输出结果调试要高效很多。不要贪图快这一类“模拟题”只要边界值每一步都是对的最终结果一定是对的。5. 一变四螺旋矩阵的经典变形思路Hot100 里螺旋矩阵这一题虽然只要求顺时针、从外到内遍历但面试官非常喜欢在此基础上出变体。这里我总结四个最常见的变形每一个都能用“边界收缩法”快速适配。5.1 变形一从中心向外顺时针遍历这类题目要求从矩阵中心开始向外螺旋扩散。核心思路是把“边界收缩法”倒过来用先找到中心点然后不断扩大边界。具体做法是先确定中心点的初始位置。如果m和n都是奇数中心点就在(m // 2, n // 2)如果存在偶数维度中心通常是四个中间格点之一或一条中轴线具体要看题意。然后维护top、bottom、left、right四个边界从中心所在的区域开始每遍历一圈就让四个边界向外扩展一格直到覆盖整个矩阵。这种题的难点在于初始边界的确定和奇偶行列的处理但底层的遍历逻辑和原题一模一样。有时间的话建议手动推一个 4×4 矩阵从中心出发的路径做完之后对这种反向边界收缩会有更直观的感受。5.2 变形二逆时针螺旋遍历把顺时针改成逆时针本质上就是调换四条边的访问顺序。顺时针是“上→右→下→左”逆时针则是“左→下→右→上”。边界更新的逻辑完全不变变的只是哪个for循环先执行。实际写代码时可以先把顺时针版本写出来再把四条边的顺序全部倒过来并相应调整每个for循环的起点和终点。比如逆时针的第一条边是“从上到下遍历左列”对应代码就是把原来“从左到右遍历上边”的循环改成遍历左列。5.3 变形三生成螺旋矩阵LeetCode 第 59 题“螺旋矩阵 II”就是这道题的逆操作给你一个正整数n生成一个n × n的矩阵里面按顺时针螺旋顺序填充从 1 到n*n的数字。解法思路就是把边界收缩法的遍历逻辑保留把“读取元素”改成“写入元素”。初始化一个全零的二维数组维护top、bottom、left、right四个边界再维护一个计数器num在每一条边的遍历过程中依次给matrix赋值。循环结束条件、边界更新规则、保护判断全部和原题一致。5.4 变形四不完全矩形不规则的矩阵或单链表结构有些题目会把螺旋遍历应用到不规则结构上比如锯齿形矩阵、稀疏矩阵甚至把矩阵压缩成某种链表形态后再螺旋读取。碰到这类题边界收缩法依然能用但需要先确认“矩形区域”的边界到底如何划定。如果是不规则矩阵可以先在逻辑上补齐成一个矩形遍历时用哨兵值或空判断跳过无效区域。这种处理方式在工程上非常常见属于“用空间换逻辑简单性”的思路。6. 从这道题延伸出去的刷题方法论很多人刷 Hot100 喜欢按题号顺序来一道一道往下做。但我的感觉是Hot100 的价值在于帮助建立“题型-解法”的映射。螺旋矩阵看着只是一道中档题但把它和方向模拟类问题放在一起对比学习效果会好很多。6.1 方向模拟类问题的通用套路螺旋矩阵属于“模拟过程类”题目。这类题目还有一个常见变种是机器人从某个起点出发按指令在网格中移动。它们的共同点是需要用一个状态变量记录当前方向遇到边界或障碍时切换状态。在 LeetCode 上这类题还有第 874 题“模拟行走机器人”、第 657 题“机器人能否返回原点”等。放在一起刷你会慢慢总结出规律模拟题的基本盘就是变量管理只要把每个循环变量的变化过程和终止条件列清楚基本不会出错。6.2 如何用“三遍法”扎实掌握这类题针对螺旋矩阵我建议你按三步走第一遍照着文章里的代码自己手动敲一遍跑通示例用例。此时你可能只是照葫芦画瓢没关系目标是熟悉语法和整体结构。第二遍合上代码自己从头写。写不出来就回去看思路图重点看循环边界和更新顺序。这遍写完你基本能独立搞定原题。第三遍把顺时针改成逆时针、把读取改成填充、把外到内改成内到外。每一遍改动都是对思路的加压测试。三遍走完螺旋矩阵的题对你来说就是肌肉记忆。6.3 刷题时的“时间盒”策略有不少人刷题容易陷入“一道题死磕一整天”的状态。我不太推荐这种搞法。对螺旋矩阵这类模拟题我给自己定的规则是如果 30 分钟内没有任何有效进展就去看题解但看完后必须自己重新写一遍并且尝试换一种写法。这种策略的核心是不要让挫败感代替思考。先看题解学会一种思路再通过独立复现把思路内化成自己的。实事求是地说真正高效的刷题者不是不碰题解而是把题解当“拐杖”而不是“答案”。7. 写在最后的一点经验螺旋矩阵这道题我在刷题初期其实很不以为然觉得就是简单的数组遍历。直到有一次模拟面试面试官让我现场写我第一轮就写崩了——边界更新顺序搞反输出结果花里胡哨。那时候我才意识到越是这种看似简单的题越考验基本功。后来我把边界收缩法练熟之后遇到螺旋矩阵的变形题基本都能在几分钟内写出可运行的代码。个人体会是这道题最值得收藏的不是某一种写法而是“四个边界变量管理遍历范围”这个思维模型。这个模型在矩阵类题目里通用性极强从二维数组的按层操作到图像处理里的区域遍历都能用上。如果你正在刷 Hot100建议不要只看题解一定要亲手把 3×3、3×4、1×4、4×1 这几种矩阵都跑一遍。跑的过程就是理解边界条件的过程踩过一次坑之后比背十遍代码都管用。