螺旋矩阵全解析:四指针收缩法与边界条件详解

发布时间:2026/9/28 12:54:40
螺旋矩阵全解析:四指针收缩法与边界条件详解 1. 螺旋矩阵到底在考什么——题面拆解与核心考点螺旋矩阵这个题几乎是每一份算法面试题库里都逃不掉的标准角色。它表面上是一个二维数组遍历题给定一个 m 行 n 列的矩阵要求按顺时针螺旋顺序返回矩阵中的所有元素。我第一次见到它的时候觉得挺直观的就是在里面绕圈走呗。但真正动手写才发现这个“绕圈”的过程里藏着一堆边界条件、循环终止判断和各种奇偶情况的处理稍不留神就是 index out of range 或者漏掉一行。它之所以被反复用于面试是因为它并不依赖什么特别高深的算法模型不考 DP 状态转移也不考链表指针舞蹈它考察的是你把直觉转化成可执行逻辑的能力。具体来说有三个点方向控制、边界收缩和循环终止条件的判断。这三个点恰恰是工程代码里最常见的 bug 来源。1.1 核心难点方向变化与边界收缩的同步很多人一开始想的是我用一个visited布尔数组记录走过的地方然后按照“右、下、左、上”的顺序依次尝试前进如果下一步越界或者已经访问过就切换到下一个方向。这样当然能解思路很直白代码写出来也不难。但它有两个问题一是额外开了一个 m x n 的布尔矩阵空间复杂度是 O(mn)二是这个写法在面试时要解释“为什么这样不会死循环”其实需要多绕几句话。而更干净的做法是“四指针收缩法”维护top、bottom、left、right四个边界变量每一圈遍历完之后把对应的边界往中间收一格。这个思路的直觉类比就是剥洋葱一层层撕掉外皮。每一层剥的时候因为你已经明确知道四个边界在哪里就不需要visited数组了。空间复杂度降到 O(1)代码逻辑也紧凑很多。这个过程里最容易错的地方在于“什么时候不剥那一层只剩最后一个元素”的判断。比如一个 3 行 4 列的矩阵剥掉最外层之后剩下的是一个 1 行 2 列的内层这个内层只有横向一条线如果还按“右、下、左、上”的流程走就会在下行或者左行这一步重复遍历已经访问过的元素。1.2 两类主流解法方向向量法 vs 四指针收缩法方向向量法就是预先定义好方向数组DIRS [(0, 1), (1, 0), (0, -1), (-1, 0)]用一个direction_index在 0 到 3 之间循环每次遇到边界或者已经访问过的格子就切换方向。它的优点是好理解而且顺着这个思路很容易延伸出“机器人行走”“迷宫寻路”一类的问题。缺点前面也说过需要visited数组空间占用不是最优。四指针收缩法则是面试里我更推荐优先写出来的方案。它不依赖额外数组每一圈的处理套路完全一致只不过在“左到右、上到下、右到左、下到上”这四段遍历之后需要额外判断“是否只剩一行”或“只剩一列”。这个判断是很多人在白板上卡住的地方。下面我把这个思路的完整推演展开讲清楚每一步为什么这么写。2. 从零开始手写螺旋矩阵——完整推导与代码实现以 LeetCode 54 题为例输入是一个 m x n 的矩阵输出是按顺时针螺旋顺序排列的元素列表。这里注意题目没有说 m 和 n 相等所以你的代码必须同时处理好方阵和非方阵。2.1 四指针收缩法的核心逻辑我们定义四个变量top当前尚未遍历区域的最上面行的索引初始为 0bottom当前尚未遍历区域的最下面行的索引初始为 m - 1left当前尚未遍历区域的最左列的索引初始为 0right当前尚未遍历区域的最右列的索引初始为 n - 1每一轮循环做四段遍历从left到right遍历第top行。结束后top 1从top到bottom遍历第right列。结束后right - 1从right到left遍历第bottom行。结束后bottom - 1从bottom到top遍历第left列。结束后left 1循环的入口条件是top bottom and left right。外层循环每跑一圈矩阵的外围一圈就被“剥”下来内层区域继续下一轮。这里我要特别强调第 3 步和第 4 步前的判断如果当前只剩下单行或者单列这两步就不要执行了。原因是前面第 1 步、第 2 步已经把这一行或这一列的元素全部遍历掉了如果继续执行第 3、4 步就会重复读已经放进结果集的元素。2.2 代码实现与逐行注释def spiral_order(matrix): if not matrix or len(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. 从右到左遍历当前底部行 # 这里必须判断 top bottom因为经过第一步后 top 可能已经越过 bottom if top bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom - 1 # 4. 从下到上遍历当前左侧列 # 这里必须判断 left right因为经过第二步后 right 可能已经越过 left if left right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left 1 return res这个版本的代码我建议你直接背下来不是死记硬背而是把它的结构和每一步的边界处理逻辑记住。面试中如果能流畅写出并且解释清楚条件判断的原因是很加分的。2.3 非方阵的边界陷阱m 与 n 不相等时的表现如果矩阵是 3 行 3 列也就是 n x n 的方阵那么每一轮的四个边界收缩都比较对称不容易出问题。但如果是 3 行 4 列第一轮剥完外圈后top 1、bottom 1、left 1、right 2这时只剩下中间一行两个元素。进入第二轮循环后第一步从左到右遍历了matrix[1][1]和matrix[1][2]然后top变成 2此时top bottom所以第三步的“从右到左遍历底部行”必须跳过否则会重复遍历。这正是上面代码在两个地方加上if top bottom和if left right的原因。再举一个极端1 行 5 列的矩阵。初始top 0、bottom 0、left 0、right 4。第一轮第一步遍历完整行top变成 1此时外层条件top bottom已经不满足了。第二步实际上会执行吗会但注意range(top, bottom 1)是range(1, 1)是空区间不会访问任何元素所以也不会出错。因为列表切片式的range在 start 大于 end 时天然是空的。之后第三步的if top bottom为 False跳过第四步的if left right虽然为 True但range(bottom, top - 1, -1)是range(0, 0, -1)同样是空区间。最终结果就是这一行本身。这里有一个非常实用的经验Python 的 range 天然帮你处理了一些空区间情况但千万不要依赖这个特性就省略条件判断。因为第三步和第四步里面range(right, left - 1, -1)这种倒序区间如果 right 等于 left 而元素已经遍历过了不加判断的话就一定会重复。2.4 方向向量法的对比实现def spiral_order_direction(matrix): if not matrix or len(matrix) 0: return [] m, n len(matrix), len(matrix[0]) visited [[False] * n for _ in range(m)] dirs [(0, 1), (1, 0), (0, -1), (-1, 0)] di 0 x, y 0, 0 res [] for _ in range(m * n): res.append(matrix[x][y]) visited[x][y] True nx, ny x dirs[di][0], y dirs[di][1] if nx 0 or nx m or ny 0 or ny n or visited[nx][ny]: di (di 1) % 4 nx, ny x dirs[di][0], y dirs[di][1] x, y nx, ny return res这个写法每一步都很机械不容易漏但确实多花了一个visited数组的内存。如果面试时你先写出四指针版再顺嘴提一句“想更省空间可以用边界收缩想更通用可以用方向数组”面试官会非常满意。3. 螺旋矩阵的经典变体——从遍历到填充螺旋矩阵这个考点不只出现在“遍历输出”这一种问法。面试官最爱做的是在这个基础上衍生出几个变体考察你是不是真的理解了螺旋顺序的本质还是只会背模板。3.1 变体一逆时针螺旋遍历如果是逆时针顺序也就是“上到下、左到右、下到上、右到左”的方向循环其实思路完全一样只是方向数组或者四段遍历的顺序换一下。方向向量法里把方向数组从顺时针变成逆时针即可dirs [(1, 0), (0, 1), (-1, 0), (0, -1)]四指针法里每一轮的遍历顺序变为从上到下遍历左侧列、从左到右遍历顶部行但这里要当心收缩顺序、从下到上遍历右侧列、从右到左遍历底部行。我建议你准备的时候把顺时针和逆时针都练一遍。练完你就会发现它们的本质就是“边界收缩的顺序”和“收缩前是否要判断单行单列”这两个点。3.2 变体二螺旋矩阵 II——按螺旋顺序填充这个是 LeetCode 59 题。给定一个正整数 n生成一个 n x n 的矩阵矩阵元素按照从 1 到 n² 顺时针螺旋顺序填充。这类题目代码上几乎和遍历一模一样只是把“读元素”变成“写元素”。def generate_matrix(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这里有个地方值得注意当 n 为奇数时最内层会剩下单独一个格子这个格子在“从左到右”那一步就会被填充后续的单行单列判断会跳过。当 n 为偶数时最内层是 2 x 2 的方块四步都能完整执行。两种情况下结果都是对的这是边界收缩法最漂亮的地方你不用单独处理中心元素。3.3 变体三从矩阵中心开始的螺旋遍历这个变体比前两个有意思常见于一些机试题目或者系统设计中的坐标访问场景。比如给你一个奇数 n从矩阵正中心开始按顺时针螺旋向外依次访问所有元素。思路是先确定中心坐标然后一圈一圈向外扩展。每一圈的边长是2 * k 1其中 k 从 0 递增到 (n-1)/2。这里我推荐的方向处理方式是以中心为起点先向右走一步再开始“下、左、上、右”的方向循环每走一个方向走的步数是2 * k第 k 圈总共走4 * (2k)步加上最后回到新圈起点。严格来说这不是 LeetCode 原题但它是非常好的训练材料能帮你把方向控制和步长控制分开练熟。3.4 变体四蛇形矩阵对角线遍历里也用到的步长思维蛇形矩阵问题里有一个经典操作按对角线遍历二维数组每走完一条对角线方向就换一次。它和螺旋矩阵虽然遍历顺序不同但本质上都是在二维坐标里做“方向控制 边界判断”所以把螺旋矩阵练透之后蛇形矩阵的代码写起来会顺手很多。我建议你在刷题阶段把这些问题放在一起打包练LeetCode 54 螺旋矩阵遍历LeetCode 59 螺旋矩阵 II填充LeetCode 498 对角线遍历蛇形LeetCode 2326 模拟行走如果有余力可以看这样你就不只是会了一个题目而是掌握了二维数组方向控制这一类题目的通用套路。4. 实战中的高频 Bug、边界测试与代码优化说句实话螺旋矩阵这道题我在面试别人和帮人改代码的时候亲眼见过太多种 bug 了。下面把这些高频错误整理成一个速查表你写完之后照着排查一次通过率会高很多。4.1 高频 Bug 清单与排查方法症状根本原因解决办法输出结果重复访问了一些格子缺少单行/单列判断第三步或第四步重复执行在第三、四步前加if top bottom和if left right报错 index out of range收缩边界后仍然使用了旧的边界值遍历每完成一段遍历立即更新对应边界再用新边界做下一段空矩阵时异常没有处理matrix为空或matrix[0]为空的情况开头加if not matrix判断循环死循环top、bottom、left、right更新顺序错乱导致收缩不一致按“遍历完后立即收缩”的顺序逐段更新方阵正确但非方阵出错写代码时只按 n x n 的假设来写没测试 m x n用 3 x 4、4 x 3、1 x 5、5 x 1 多跑几组用例这些 bug 里出现频率最高的就是第一种。几乎每次有人给我看他的螺旋矩阵代码跑出了重复数字问题都出在少了那两行 if 判断。4.2 测试用例设计方法从最小规模开始覆盖实际写题的时候很多人跑一两个用例就提交了结果在边界用例上挂掉。这里我给出一套我常用的测试序列空矩阵[]应该返回[]单行矩阵[[1, 2, 3, 4]]应该返回[1, 2, 3, 4]单列矩阵[[1], [2], [3]]应该返回[1, 2, 3]1 x 1 矩阵[[1]]应该返回[1]2 x 2 矩阵[[1, 2], [3, 4]]应该返回[1, 2, 4, 3]3 x 3 方阵重点观察中心元素是否访问一次且仅一次3 x 4 非方阵重点观察第二圈只剩一行时的处理4 x 3 非方阵重点观察第二圈只剩一列时的处理有了这组测试你的代码能覆盖绝大多数边界场景。我要特别提一下 4 x 3 这个用例因为很多人测了 3 x 4 就以为覆盖了“非方阵”但 3 x 4 是剥完外圈后剩一行4 x 3 剥完外圈后剩一列一个是第三步要跳过一个是第四步要跳过两者的 bug 触发点不一样。4.3 复杂度分析为什么这个解法是 O(mn)时间复杂度方面每个元素恰好被访问一次所以是 O(mn)。空间复杂度四指针法除了存储结果的res数组外只有四个整数变量所以是 O(1)如果题目要求返回结果列表那res本身就是必须的这个不算额外空间。方向向量法则多了一个visited二维数组空间复杂度为 O(mn)。面试时如果被问到“能不能不用额外空间”你就可以回答用四指针收缩法本来就不用额外空间。这个回答简洁又有力。4.4 代码细节优化减少重复遍历和提升可读性有些人在写四指针法时习惯第一步遍历完整行之后就立刻top 1然后第二步遍历列第三步再遍历底行。这个顺序完全正确但要注意第二步的列遍历区间起点应该是更新后的top而不是原来的top否则会重复访问右上角那个元素。同理第三步遍历底行时起点应该是更新后的right。为了提升可读性我推荐在每段遍历前用注释标注方向比如# 向右遍历当前 top 行 # 向下遍历当前 right 列 # 向左遍历当前 bottom 行 # 向上遍历当前 left 列这种注释在白板面试里特别管用面试官不一定要看你写得有多快但一定会看你的代码是不是 self-explanatory。5. 常见问题与面试现场经验——从写对到讲好最后这部分我把自己面试别人时最常问的追问问题和准备建议写出来。你会发现螺旋矩阵这种题目光写对可能只能拿基础分真正拉开差距的是你对边界条件的解释和对变体的应变能力。5.1 面试官最爱的几个追问第一个追问就是“为什么第三步和第四步要加条件判断”这个问题几乎是必问的。如果你能清楚解释“因为第一步或第二步之后可能已经把最后一行或最后一列遍历完了不加判断就会重复访问”面试官会点头。第二个追问是“如果矩阵是稀疏矩阵用方向向量法会有什么问题”这个问题有点难度考察的是你对空间和时间开销的理解。方向向量法需要visited数组在稀疏矩阵场景下这个数组的存储开销是可以优化的但螺旋遍历本质上绕不开对所有元素的访问所以时间复杂度没有优化空间。第三个追问是“能不能递归实现”理论上可以递归每一层调用处理完外圈后把内圈矩阵重新作为参数传入下一层递归。但要注意递归实现里你需要自己构造剩余的内层子矩阵如果直接传递原矩阵、又要带上四个边界参数代码反而更绕。所以在实际项目中我不会推荐递归面试里如果真的被问到用迭代版展示底层的边界控制能力更稳妥。5.2 准备方向和刷题顺序建议我的个人经验是这样的先把四指针法的代码背熟做到一小时之内默写两遍不出错。然后不急着刷变体先自己针对 3 x 4、4 x 3、1 x n、n x 1 这些边界写一遍测试用例。把这些边界搞定了再去做螺旋矩阵 II 填充版本你会发现代码几乎不用改思路只是把res.append(matrix[i][j])换成matrix[i][j] num。之后再练逆时针版本和中心螺旋扩展重点体会方向控制不变、遍历顺序改变带来的差异。等你能做到看到任何“螺旋”相关的题目一眼就能分辨出它考的是“遍历”还是“填充”、是“方阵”还是“矩形”、是“顺时针”还是“逆时针”的时候这个知识点你就彻底拿下了。我在实际面试别人时还注意过一个细节不少候选人在判断矩阵是否为空时只写了if not matrix但 Python 里空矩阵[[]]这种情况not matrix为 False但len(matrix[0])是 0如果代码里直接访问matrix[0][0]就会报错。所以完整的开头判断最好写成if not matrix or not matrix[0]: return []这个看似不起眼的小细节是我在实际笔试和面试中见过很多次的翻车点。根据我个人刷题和带人的经验螺旋矩阵这类题最值得你花时间的地方不在“会做”而在“讲得清”。你只需要把边界收缩、方向控制和单行单列判断这三个点讲透就已经吃透了这个经典题目的内核。那些变体题本质上都是在这些内核上做文章不会跳出这个框架。把这个基础打扎实后面的扩展和变体都会顺畅很多。