螺旋矩阵详解:边界维护、两种解法与面试变形

发布时间:2026/10/16 10:12:39
螺旋矩阵详解:边界维护、两种解法与面试变形
力扣Hot100刷到第15题正好是54. 螺旋矩阵。这道题在Hot100里并不算难但它特别能暴露一个人对边界条件的敏感度。讨论区里几乎每年都有人因为少写一个if导致单行矩阵输出重复也有人把循环条件里的写成结果整个结果串少一截。我自己第一次写这道题也改了两遍才过所以这篇文章不打算只贴一份标准答案而是把那些容易翻车的细节、调试思路、以及面试官最爱追问的变形一起讲透。如果你刚开始刷Hot100或者正刷到二维数组这一块这篇可以当作一个比较接地气的题解来看。我会讲三种视角按层模拟、方向控制、以及变形题迁移。不管你是写C、Java还是Python核心逻辑都一样代码部分我会给可直接跑的版本并解释每一步为什么这么写。1. 题目到底在问什么1.1 题意回顾从外圈到内圈的“蛇形读取”题目要求输入一个 m 行 n 列的矩阵 matrix返回一个一维数组里面装的是矩阵按照顺时针螺旋顺序展开后的元素。比如输入[[1, 2, 3], [4, 5, 6], [7, 8, 9]]输出就是[1,2,3,6,9,8,7,4,5]。再比如一个 3 行 4 列的矩阵[[1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12]]输出是[1,2,3,4,8,12,11,10,9,5,6,7]。注意第二个例子里从右边下来后第二行从右往左读的是 6 和 7而不是整行。这就是螺旋遍历和普通逐行读的本质区别它永远贴着当前未访问区域的外圈走每走完一圈就往里缩一圈。1.2 这题考的不是智商是“边界维护”很多人第一反应是用递归剥掉最外圈再把内圈矩阵递归处理。这个思路本身没错但递归并不直接反而容易在“剥到哪一层结束”上出问题。实际上螺旋矩阵的本质是一个状态变化问题只需控制当前遍历到哪四条边然后按右、下、左、上的顺序走每走完一条边就把对应的边界往里收一格。整个过程很像剥洋葱一层一层剥到中心直到没有未访问的元素为止。这类题有个共同特点算法套路固定没有贪心、动态规划那种需要灵光一现的地方。它专门考察循环不变量和临界判断。你能不能保证每一步坐标不越界、不重复、不遗漏才是这道题真正的考点。这也是它在Hot100里存在的意义——用一道简单的模拟题筛选出对代码细节敏感的人。1.3 常见的错误思路我见过不少初学者试图用“旋转矩阵”来简化每次取走第一行然后逆时针旋转剩余矩阵再取第一行。这个方法看起来优雅实际写起来需要额外复制矩阵时间复杂度会变成 O((mn) * mn)而且坐标非常容易混乱属于典型的花活。还有人想把所有元素按某个“螺旋距离”一次性计算出来。比如用 min(i, j, m-1-i, n-1-j) 判断层数再按方向定位。在小矩阵上能跑通但实现时要对四个方向分别讨论反而比模拟还复杂特别不适合笔试场景。所以我建议先老老实实把“模拟转圈”吃透再考虑其他优化。下面两种解法你面试时至少能稳稳写出一种。2. 解法一按层模拟最稳妥的写法2.1 核心思想用四个变量把矩阵框起来按层模拟需要定义四个边界变量top最上面还没有被取走的那一行bottom最下面还没有被取走的那一行left最左边还没有被取走的列right最右边还没有被取走的列初始值分别是0、m-1、0、n-1。只要top bottom且left right说明中间还有至少一个元素就继续跑一圈。每一圈按四个方向取数从(top, left)走到(top, right)这是上边界从左到右。取完之后top。从(top, right)走到(bottom, right)这是右边界从上到下。取完之后right--。从(bottom, right)走到(bottom, left)这是下边界从右到左。取完之后bottom--。从(bottom, left)走到(top, left)这是左边界从下到上。取完之后left。第三步和第四步执行前都要加一个保护判断否则当top bottom或left right时会重复读取同一行或同一列。这个判断是整个代码里最重要的细节后续会专门说明。2.2 手推一个 3 x 3 的例子拿[[1,2,3],[4,5,6],[7,8,9]]来说第一圈开始top0, bottom2, left0, right2。第一步读第0行1, 2, 3读完后top1。第二步读第2列的第1到第2行6, 9读完后right1。此时top1 bottom2执行第三步读第2行从第1列到第0列也就是 8, 7读完后bottom1。此时left0 right1执行第四步读第0列从第1行到第1行也就是 4读完后left1。循环条件判断top1 bottom1且left1 right1进入第二圈。第一步读第1行第1列5读完后top2。第二步时top2 bottom1不成立跳过第三步、第四步同理都跳过。循环结束输出[1,2,3,6,9,8,7,4,5]。注意第二圈第一步只读了 5因为当top2后已经超过bottom1后面三步必须靠条件保护住否则会把已经读过的元素再读一遍。2.3 可运行的 Python 代码def spiralOrder(self, matrix: List[List[int]]) - List[int]: 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第三、四步的if是整个代码里唯一不能省的地方。等会儿我会用单行单列的例子说明为什么。2.4 Java 版本参考如果你常用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; int top 0, bottom m - 1; int left 0, right n - 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 只需要把 Java 的ListInteger换成vectorintres.add换成res.push_back其他完全一样。核心逻辑不依赖语言所以不用死记每种语言的写法记一种然后熟悉对应语法就行。2.5 复杂度很干净遍历过程中每个元素只会被加入结果数组一次所以时间复杂度和矩阵元素个数成正比是 O(mn)。除了结果数组之外只用了四个常数变量空间复杂度 O(1)。这里说的 O(1) 没有把返回数组算进去因为结果数组是题目要求返回的属于必要输出。面试时可以主动提一句“如果不计入输出数组额外空间是 O(1)”会显得你思路很清晰。3. 解法二方向控制 已访问标记另一种思路3.1 为什么还要学第二种写法按层模拟虽然容易理解但循环体里会出现 if还要仔细处理边界收缩。如果面试题改成“从任意起点开始顺时针转圈”或者“逆时针”第一套写法就需要大改。所以另一个常用做法是预定义方向数组每一步先尝试往前走如果发现越界或者走到已经访问过的格子就原地右转90度换下一个方向继续走。这个思路本质上是“模拟机器人前进”适合处理更灵活的遍历场景。3.2 方向如何定义顺时针的四个方向依次为右(0, 1)下(1, 0)左(0, -1)上(-1, 0)维护一个方向序号dd0表示当前方向是右d1下d2左d3上。每走一步先计算下一步位置ni i dirs[d][0]nj j dirs[d][1]。当下一步越界或者对应的visited标记为 True就执行d (d 1) % 4然后重新计算下一步位置。因为题目固定从左上角开始、顺时针走你可以把方向序号初始化为 0完全契合。3.3 代码实现def spiralOrder(self, matrix: List[List[int]]) - List[int]: 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)] # 方向右、下、左、上 dirs [(0, 1), (1, 0), (0, -1), (-1, 0)] d 0 i j 0 res [] for _ in range(total): res.append(matrix[i][j]) visited[i][j] True ni, nj i dirs[d][0], j dirs[d][1] if ni 0 or ni m or nj 0 or nj n or visited[ni][nj]: d (d 1) % 4 ni, nj i dirs[d][0], j dirs[d][1] i, j ni, nj return res这里循环次数直接固定为总元素数total所以不用担心多走。每次走向下一个位置前如果原方向已被访问或越界就转向。这样做省去了对 top/right/bottom/left 的判断代价是需要额外 O(mn) 的 visited 空间。有人会问既然目前方向不对右转90度之后还需要再检查一次吗不需要。因为矩阵是矩形而且我们的访问路径是连续螺旋右转后的方向一定是合法且未访问的。这一点是“标准螺旋”的性质不是突发奇想。3.4 两种解法怎么选对比项按层模拟方向控制思路直观程度高四边边界清晰中靠方向数组和转向逻辑需要维护的变量4个边界变量方向数组 visited矩阵额外空间O(1)O(mn)代码出错点第三、四步的if方向序号和转向条件应对变形能力一般更强改方向初始值即可两种方法都能过原题。面试时如果能说出两种通常会让面试官觉得你对边界条件理解得更全面。我见过有面试官在螺旋矩阵基础上追问“如何逆时针”“如何从中心开始向外螺旋”方向控制明显更好改。但如果没追问按层模拟已经足够。4. 边界情况与调试实录4.1 最容易翻车的三处细节第一处第三步和第四步的保护条件。当矩阵只有一行时top bottom。第一圈第一步把这一行读完top变成bottom1。第二步虽然 for 循环的范围range(top, bottom 1)因为top bottom不会执行但right已经减1。如果没有if top bottom第三步for j in range(right, left - 1, -1)会把这一行从右往左再读一遍。同理只有一列时第四步会重复读。这是这题提交区最常见的错误原因。第二处边界变量的更新顺序。每次取完一条边边界变量要立刻更新后续步骤必须使用更新后的边界。比如第一步取完上边界后如果忘记top第二步在右边界遍历时会把第一行右端的元素再读一次。第三处循环结束条件。有人喜欢写while top right当矩阵不是正方形时会漏掉某些行。正确的循环条件应该同时限定行和列top bottom and left right。4.2 单行、单列、空矩阵怎么处理空矩阵需要单独拦截代码开头就判断了if not matrix or not matrix[0]。单行输入比如[[1,2,3]]走一遍按层模拟初始topbottom0left0right2。第一步读 1, 2, 3top变 1。第二步right变 1因为range(1, 1)为空不执行。第三步if top bottom为 False跳过。第四步if left right为 True此时left0right1所以会进入循环但range(bottom, top - 1, -1)即range(0, 0, -1)为空不会读重复。结果正确。虽然第四步没有造成实际影响但逻辑上能绕过就绕过。我写代码时习惯把第三步和第四步都加上if这样阅读时不用做额外推导也不担心后续改动破坏结构。单列输入同理比如[[1],[2],[3]]第三步的保护尤其关键。如果只有一列第一步读 1第二步读 2、3第三步如果没有if top bottom会尝试从下往上再读一遍直接把结果弄乱。4.3 用打印法快速定位问题刷题时我最常用的调试方式是临时在循环开头打印top、bottom、left、right以及当前res的值。比如在一个 3 x 4 矩阵上跑一遍你能清楚看到每一圈过后边界怎么收缩。总结一下排查方向出现重复元素大概率是漏了if保护出现漏元素大概率是循环条件写窄了出现死循环大概率是某个分支里边界变量没有被更新。这三种情况基本覆盖螺旋矩阵的绝大多数 bug。4.4 我在本地调试时的个人习惯我会准备一组最小测试用例不是直接跑示例而是跑 1x1、1x3、3x1、2x3、3x2 这些形状。特别是 1x3 和 3x1专门验证有没有重复读取。如果这几个用例都通过再跑标准 3x3 和 3x4基本就稳了。写代码时我还会刻意把边界变量拆小每一个 for 循环都单独用一行注释标注当前方向。这样做虽然多敲几个字但后续排查会省很多时间。别小看这个习惯很多人在白板上写这题时就是因为没有注释引导写到第三步就不知道自己走到哪了。5. 面试追问与题目变形5.1 如果要求改成逆时针怎么办逆时针的顺序是下、右、上、左。按层模拟做法先从上到下遍历left列然后left从左到右遍历bottom行然后bottom--从下到上遍历right列然后right--从右到左遍历top行然后top。每一步同样需要加保护判断。方向控制则只需要把方向数组调整为[(1,0),(0,1),(-1,0),(0,-1)]初始方向设为0。这个改动非常快也是方向控制方案的优势所在。5.2 螺旋矩阵 II从读取矩阵变成生成矩阵面试官可能顺势反问给定 n生成一个 n x n 的矩阵元素从 1 到 n^2 按螺旋顺序填充。核心思路完全一致只是把res.append(matrix[i][j])改成matrix[i][j] value然后value 1。如果你理解了按层模拟这道生成题十分钟能写完。方向控制也一样每次把当前格子填上数字再沿原方向移动遇到边界或已填就转向。建议把这两题放一起刷一个读一个写边界维护的感觉会更牢。5.3 向“旋转图像”和“对角线遍历”迁移除了螺旋矩阵II矩阵类题目还有两个常见变种旋转图像和对角线遍历。旋转图像虽然更常用“转置翻转”但如果你能先熟练处理边界收缩看到分层问题时会有更强的大局观。个人推荐刷完这题后把同一个题单里的旋转图像、对角线遍历一起看掉。它们都属于二维数组遍历技巧放一起刷效率很高。不要只背代码要重点理解“每一步之后问题的规模如何缩减”。螺旋矩阵每处理完一圈问题规模就从 m x n 变成了 (m-2) x (n-2)旋转图像每处理完一层规模同样减2。这种“收缩思维”在很多矩阵题里都通用。5.4 这道题在 Hot100 里的定位Hot100 是很多人的刷题路线图前十几题通常是哈希、链表、双指针螺旋矩阵能排到15题左右说明它在数组类基础题里很有代表性。它不涉及高级数据结构也没有复杂的状态转移方程唯一需要你做的就是耐心模拟。对正在刷 Hot100 的人我建议不要因为这道题简单就快速跳过。试着用两种解法各写一遍再思考如果矩阵不是矩形而是“之字形”阶梯状程序要怎么改。这个过程比单纯背一遍答案收获大得多。6. 刷完这道题后我建议你再想三件事第一怎么调试二维遍历代码核心不是靠人肉模拟整个矩阵而是缩小规模、打印边界、逐步验证。我至今还保持这个习惯遇到越界问题先看是不是方向数组写错遇到重复问题先看是不是少写了 if。第二怎样才算真正“会”一道题能写出来只是第一步能跟面试官解释清楚为什么在单行/单列时不会重复才是真正理解。这道题里边界变量收缩的顺序、保护条件的由来都是值得复述的核心知识点。第三矩阵类题目有没有通法有的。很多矩阵问题都可以用一个“状态 更新”的模型来表达当前状态就是坐标和方向每次前进前检查下一步是否合法不合法就换状态。这个模型在螺旋矩阵、岛屿问题、机器人路径规划里都能用。想通这一点以后再遇到类似的遍历题你会感觉是在做变体而不是做新题。我个人在实际刷题中最受用的一个习惯是把 3x3、单行、单列三个用例用笔在纸上各画一遍再动手写代码。螺旋矩阵这道我前前后后写了很多遍现在闭眼也能写出来但每次遇到它还是会再警惕一次那两行 if。希望你也能把这层窗户纸捅破之后再看这类题就通透了。