全解:两种解法与边界条件详解)
刷 LeetCode 的时候经常能看到一类让初学者又爱又恨的题目明明逻辑不复杂代码一写就出错边界条件绕得人头晕。“蜗牛排序”就是其中最有代表性的一个它的正式名字叫螺旋矩阵Spiral Matrix对应 LeetCode 第 54 题。我第一次做这道题的时候花了一个多小时才把边界条件理顺提交了五次才通过。后来在面试里又遇到它的变体才发现这套“绕圈遍历”的思维几乎是所有二维数组题目的地基。这篇文章我打算把蜗牛排序彻底讲透从核心思路、两种主流解法到边界条件的处理细节再到常见的变形题和坑点一次性说清楚。无论你是刚开始刷题的新手还是准备面试想在代码题上少踩坑的开发者这篇题解都能给你一个可以直接“抄作业”的完整方案。1. 蜗牛排序到底是什么问题定义与核心难点1.1 题面拆解顺时针螺旋遍历的规则先看原题描述给你一个 m 行 n 列的矩阵 matrix请按照顺时针螺旋顺序返回矩阵中的所有元素。什么叫顺时针螺旋顺序你可以想象自己在矩阵的左上角出发先沿着顶边往右走走到头之后往下走走到头之后往左走走到头之后往上走然后再往右走……像一只蜗牛缩进壳里时留下的轨迹一圈一圈往中心收缩直到把所有元素都访问完。举个例子一个 3x3 的矩阵[ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]螺旋遍历的结果就是 1, 2, 3, 6, 9, 8, 7, 4, 5。再比如 3x4 的矩阵[ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12] ]结果是 1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7。看明白了吗整个过程就是“右 → 下 → 左 → 上”四个方向不断循环每走完一条边这条边所在的“墙”就往内收缩一格。这个“收缩边界”的理解方式正是所有解法的灵魂。1.2 为什么这道题容易卡住三个核心难点我辅导过不少朋友刷这道题发现大家卡住的地方高度一致基本都是这三点第一个难点是方向转换的时机。什么时候该从往右走变成往下走不能等到撞墙才转弯而是要在“已经走到了这条边的尽头”时立刻转弯。这个“尽头”不是矩阵的物理边界而是“当前有效边界”——因为随着遍历的进行上、下、左、右四条边界都在不断往中心收缩。第二个难点是边界收缩的时机。走完上边之后上边界要 1走完右边之后右边界要 -1。这个收缩动作必须在正确的时机执行否则下一轮遍历就会把已经访问过的元素再走一遍。第三个难点也是大家最常犯的错——终止条件的判断。什么时候说明整个矩阵已经遍历完了很多人想当然地认为是“循环了 m * n 次”或者“四个边界都交叉了”但实际上写代码的时候这两种判断方式各有各的坑稍不注意就死循环或者漏元素。2. 解法一四指针边界收缩法最推荐2.1 思路总览用四个变量圈出“合法活动范围”四指针边界法是这道题最经典的解法定式。核心思想非常简单用 top、bottom、left、right 四个整型变量分别表示当前“还没被访问过的元素”所在的有效区域边界。top 初始为 0指向最上面一行bottom 初始为 m - 1指向最下面一行left 初始为 0指向最左边一列right 初始为 n - 1指向最右边一列每次遍历一条边就把对应的边界向中心收缩一格自左向右遍历 top 行遍历完后 top自上向下遍历 right 列遍历完后 right--自右向左遍历 bottom 行遍历完后 bottom--自下向上遍历 left 列遍历完后 left只要 top 和 bottom 还没交错且 left 和 right 还没交错就继续循环。这个方法的优势在于逻辑直观、不容易绕晕而且四个方向的遍历代码是完全对称的写起来很顺。2.2 完整代码实现与逐行注释Python 版我平时用 Python 刷题比较多先给出一版完整的 Python 实现每一行都标注了注释。def spiral_order(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 # 第三步遍历下边界从右到左需要先检查是否越界 if top bottom: for col in range(right, left - 1, -1): res.append(matrix[bottom][col]) bottom - 1 # 第四步遍历左边界从下到上需要先检查是否越界 if left right: for row in range(bottom, top - 1, -1): res.append(matrix[row][left]) left 1 return res2.3 为什么第三、四步之前要加条件判断这段代码里最关键、也最容易被忽略的就是第三步和第四步开头的 if 判断。很多新手抄代码的时候会漏掉这两行然后运行一两个测试用例发现没问题直到碰上特殊形状的矩阵才炸锅。我们来看一个真实的反例——单行矩阵比如[[1, 2, 3, 4]]。初始化 top 0bottom 0left 0right 3进入循环后第一步把第一行从左到右全部遍历完res 变成 [1, 2, 3, 4]然后 toptop 变成 1。此时 top 已经大于 bottom 了。如果没有 if 保护第二步会去遍历 right 列但此时range(top, bottom 1)也就是range(1, 1)是空循环不会执行。真正出问题的是第三步它会执行for col in range(right, left - 1, -1)也就是从第 3 列回退到第 0 列把已经访问过的 4, 3, 2, 1 又访问一遍导致输出结果为 [1, 2, 3, 4, 4, 3, 2, 1]。原因在于当矩阵只有一行时第一步执行完整个矩阵就已经遍历完了。此时如果不做任何判断继续执行后面的步骤就会在“已经不存在有效元素”的区域里重复访问。所以第三步和第四步前的 if 判断本质上是防止在某个方向上已经没有可遍历元素时做了多余的操作。2.4 手动模拟全过程3x3 矩阵单步追踪只看代码可能还不够直观我带着你手动跑一遍 3x3 的例子每一步都列出四个边界值和 res 的变化。初始状态top0, bottom2, left0, right2res[]第一轮循环第一步遍历 top 行col 从 0 到 2依次取 matrix[0][0]1, matrix[0][1]2, matrix[0][2]3res[1,2,3]然后 top 变为 1。第二步遍历 right 列row 从 1 到 2依次取 matrix[1][2]6, matrix[2][2]9res[1,2,3,6,9]然后 right 变为 1。第三步判断 topbottom12成立遍历 bottom 行col 从 1 到 0依次取 matrix[2][1]8, matrix[2][0]7res[1,2,3,6,9,8,7]然后 bottom 变为 1。第四步判断 leftright01成立遍历 left 列row 从 1 到 1取 matrix[1][0]4res[1,2,3,6,9,8,7,4]然后 left 变为 1。第一轮循环结束四个边界值分别是 top1, bottom1, left1, right1检查循环条件 11 且 11满足继续第二轮。第二轮循环第一步遍历 top 行col 从 1 到 1取 matrix[1][1]5res[1,2,3,6,9,8,7,4,5]top 变为 2。第二步遍历 right 列row 从 2 到 1range(2, 2) 为空不执行right 变为 0。第三步判断 topbottom21不成立跳过。第四步判断 leftright10不成立跳过。循环条件检查top2, bottom121 不成立循环结束返回 [1,2,3,6,9,8,7,4,5]完全正确。3. 解法二方向向量 访问标记法适合变体3.1 思路总览让蜗牛自己判断下一步怎么走如果说四指针法是“用四面墙圈住蜗牛”那么方向向量法就是“给蜗牛装一个导航”蜗牛每次走一格先按当前方向试着往前走如果发现下一步会走出矩阵边界或者走到已经访问过的格子就顺时针转 90 度再走一步。这个思路更接近模拟蜗牛真实爬行的过程代码也更简洁尤其适合处理不规则形状的矩阵或者非矩形遍历场景。方向向量的定义是关键。我们按“右、下、左、上”四个方向依次定义directions [(0, 1), (1, 0), (0, -1), (-1, 0)] dir_idx 0右(0, 1)行坐标不变列坐标 1下(1, 0)行坐标 1列坐标不变左(0, -1)行坐标不变列坐标 -1上(-1, 0)行坐标 -1列坐标不变每次移动前先计算下一步的行列坐标判断是否越界或者已经被访问过如果没问题就走过去否则把 dir_idx 更新为 (dir_idx 1) % 4切换到下一个方向再试。3.2 用布尔矩阵记录访问状态既然要判断“是否已经访问过”就需要一个同样大小的布尔矩阵来记录状态。在 Python 里可以用列表推导式一行创建visited [[False] * n for _ in range(m)]遍历的总次数是 m * n所以用一个 for 循环精确控制次数即可不需要额外判断循环条件。每次取出当前格子的值加入结果再标记为已访问然后尝试走下一步。如果下一步无效就转向。下面是完整的 Python 实现def spiral_order(matrix): if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) visited [[False] * n for _ in range(m)] directions [(0, 1), (1, 0), (0, -1), (-1, 0)] dir_idx 0 row col 0 res [] for _ in range(m * n): res.append(matrix[row][col]) visited[row][col] True next_row row directions[dir_idx][0] next_col col directions[dir_idx][1] if (next_row 0 or next_row m or next_col 0 or next_col n or visited[next_row][next_col]): # 换方向 dir_idx (dir_idx 1) % 4 next_row row directions[dir_idx][0] next_col col directions[dir_idx][1] row, col next_row, next_col return res3.3 两种解法的适用场景对比这两种解法各有各的适用场景不能说谁一定更好。我这里有一个选择题标准供你参考四指针边界法更适合“标准矩形矩阵的螺旋遍历”因为它不需要额外的空间除了结果数组而且边界收缩的逻辑非常清晰复杂度也是最优的。方向向量法在代码层面更简洁容易记忆而且在处理“螺旋遍历的变体”——比如从任意点出发螺旋访问、给定步数螺旋走位、不规则区域的螺旋遍历——时更灵活因为它只关心“当前位置”和“下一步是否合法”不需要维护四个边界。如果你在实际面试中被问到这类题我建议优先用四指针法因为它在空间复杂度上更优而且面试官更容易认可。如果题目变形严重、边界不规整再切换到方向向量法。4. 变体与应用场景蜗牛排序的“亲戚们”4.1 螺旋矩阵 II从遍历到生成同一个核心LeetCode 第 59 题是“螺旋矩阵 II”要求给定一个正整数 n生成一个 n x n 的矩阵矩阵元素按螺旋顺序填充 1 到 n^2。这道题可以看作是“蜗牛排序”的逆向操作原来是把矩阵元素读出来现在是把数字写进去。解法也非常相似同样是四个指针收缩边界或者用方向向量模拟走位。核心区别在于之前读元素是取出 matrix 的值现在是往矩阵里赋值。用四指针法可以这样写def generate_matrix(n): matrix [[0] * n for _ in range(n)] top, bottom, left, right 0, n - 1, 0, n - 1 num 1 total n * n while top bottom and left right: for col in range(left, right 1): matrix[top][col] num num 1 top 1 for row in range(top, bottom 1): matrix[row][right] num num 1 right - 1 if top bottom: for col in range(right, left - 1, -1): matrix[bottom][col] num num 1 bottom - 1 if left right: for row in range(bottom, top - 1, -1): matrix[row][left] num num 1 left 1 return matrix如果不放心可以用 num total 终止不过在标准解法里四边界条件就足够保证正确性了。4.2 螺旋矩阵 III、IV 与更多延伸变形LeetCode 第 2326 题“螺旋矩阵 IV”是把一个链表按螺旋顺序填入矩阵核心思想的“方向向量 边界判断”完全一致只是数据来源变成了链表节点的值。第 885 题“螺旋矩阵 III”甚至允许从任意起点开始在无限大的网格上螺旋走位这时候四指针法就不适用了必须用方向向量法配合“越界就跳过遇到未访问的合法格就收集”的方式来处理。这些变体共同指向一个事实蜗牛排序的本质不是“四个指针收缩”而是“按固定方向序列移动遇到边界或已访问区域就转向”。只要抓住这个本质不管题目怎么变都能快速找到解题方向。4.3 蜗牛排序的真实业务场景与实战价值有人可能会问刷这道题除了面试还有什么实际用处其实这个遍历模式在真实项目里还真不少见。比如图像处理里的“蛇形扫描”或者“螺旋扫描”在 JPEG 编码的 DCT 系数排列阶段就有类似逻辑目的是把二维频域数据按能量从高到低排列以优化熵编码的效率。再比如游戏开发里的地图遍历逻辑有些 Rogue-like 游戏的房间生成算法会使用螺旋搜索策略来从玩家位置向外寻找合适的生成点。另外在数据可视化领域热力图的色阶填充也偶尔会用到螺旋填充逻辑。所以这类题目练的不只是“背模板”而是理解“二维空间遍历的路径控制”这项底层能力。掌握了蜗牛排序再遇到蛇形遍历、之字形层序遍历、马鞍形遍历都能举一反三。5. 常见错误与排查笔记附速查表5.1 我踩过的三个坑和修复过程先说第一个坑忘记第三步和第四步的 if 保护。这个问题我在 2.3 节里已经详细分析了这里再补充一个更阴间的例子——单列矩阵。如果矩阵是[[1], [2], [3]]初始化 top0, bottom2, left0, right0。第一步会把第一列的第一个元素取出来然后第二步也就是从上到下遍历这一列取出 2 和 3。此时如果不加 if 保护第三步就会从 right 到 left 遍历一行把已经取过的元素再取一遍。第二个坑在 for 循环里动态更新上下界导致范围错乱。有些人写的时候会把 range 的边界写错比如写for col in range(left, right)而不是range(left, right 1)结果发现拐角元素永远取不到。这个问题的根源在于对 Python range 左闭右开特性不敏感建议在写边界遍历时反复确认“这个 range 是否把最后一个元素包含进来”。第三个坑忘掉矩阵为空或只有一行/一列的防御性判断。我见过不少人代码逻辑写得很顺但一跑matrix []就报错因为matrix[0]越界了。所以在函数开头加if not matrix or not matrix[0]: return []是成本最低、收益最高的防御手段。5.2 高频异常场景与排查速查表症状可能原因解决方案结果重复输出元素第三步/第四步缺少 if 边界保护在遍历下边、左边之前检查 topbottom 和 leftright结果缺少拐角元素range 右边界写成 right 或 bottom漏掉终点元素改为 range(left, right 1)、range(top, bottom 1)程序陷入死循环循环终止条件写错比如用 while top bottom改为 while top bottom and left right传入空矩阵报错缺少空数组防御函数开头检查 if not matrix or not matrix[0]非方阵结果错误对矩形矩阵套用方阵解法循环次数条件错误用 while 边界判断不要假设 m n输出顺序颠倒方向序列写成了 左→下→右→上确认方向顺序是 右→下→左→上5.3 一个小众但实用的排查技巧如果你在本地调试的时候发现结果不对但一眼又看不出是哪里出错我建议你写一个短小的可视化辅助函数把当前四边界和每一次 res 的元素打印出来。比如def debug_spiral(matrix): print(matrix size:, len(matrix), x, len(matrix[0])) # ... 在循环内 print(top, bottom, left, right, res)通过观察边界值的变化过程你几乎可以在 3 步之内定位到是收缩时机的问题还是范围边界的问题。这个方法看似笨拙但在现场调试时比盯着代码发呆高效得多。6. 复杂度分析与优化方向6.1 时间与空间复杂度的硬性分析四指针法和方向向量法的核心操作都是“访问矩阵中的每一个元素一次且仅一次”所以时间复杂度都是 O(m * n)这个没有任何争议。空间复杂度上四指针法除了存放结果的 res 数组只用了四个整型变量额外空间是 O(1)方向向量法多了一个 visited 布尔矩阵额外空间是 O(m * n)。在严格要求空间性能的题目里四指针法明显更优这也是为什么我建议面试优先用四指针法。6.2 空间复杂度还能不能再优化方向向量法能不能省掉 visited 矩阵同时又保持“自动转向”的写法在矩形矩阵的标准螺旋遍历里其实可以靠“下一步是否在四边界范围内”来判断也就是把 top、bottom、left、right 和方向向量法结合起来。但这样写代码反而变得复杂失去了方向向量法“实现简单”的优势。所以我的建议是为了可读性和稳定性方向向量法就老老实实带 visited 矩阵为了极致空间优化就用四指针法不要混合写。6.3 关于 LeetCode 内存表现的一点心得很多新手看到 LeetCode 提交页面上显示“内存击败 10%”就开始焦虑其实不用太在意这个指标。大多数情况下内存表现差异来自语言运行时的固定开销和结果数组本身而不是你的额外空间。在 Python 里res []列表的动态扩容、visited矩阵的每行列表对象创建都会影响内存统计但这些影响通常在数据量大时才值得关注。刷题阶段优先保证代码正确、思路清晰分析复杂度时算清楚理论值就够了。7. 拓展延伸从蜗牛排序学到的高阶思维7.1 状态机与方向切换的通用思想蜗牛排序里的方向切换方案本质上是一个有限状态机。当前状态是“方向”输入是“下一步是否合法”输出是“继续当前方向”或“切换到下一个方向”。这种状态机思想在工程领域非常常见比如自动驾驶中的车辆路径规划、机器人清扫路径规划、游戏 AI 的巡逻路线设计都能看到类似的路子。所以读懂蜗牛排序你其实就掌握了一种“在受限空间内按规则探索”的建模方式。以后再遇到“给定一片区域如何不漏不重地遍历每个位置”这类问题时会自然而然地想到方向向量 状态切换这套思路。7.2 从二维到多维的扩展思考如果要你实现三维数组的螺旋遍历你会怎么做同济大学某年考研题里就出现过类似思维题。从二维扩展到三维核心思路不变但方向从 6 个上下左右前后开始依次切换每次走完一个面就收缩一个面的边界。计数器从 2 变成 3判断条件从一个二维边界矩形变成三维边界盒子。你会发现蜗牛排序的内核在更高维度依然成立。7.3 如何处理“访问已访问区域”这个核心命题很多人解这道题时容易忽略一个问题为什么需要判断“是否已访问”因为在螺旋路径中转向的发生条件除了“撞到矩阵边界”还有“撞到自己走过的路径”。这个概念在无人车或者扫地机器人的路径规划里对应的就是“障碍物”和“已清扫区域”的判断。理解了这两个条件的并集你就理解了蜗牛排序的全部逻辑要么碰到外墙要么碰到自己的痕迹就转弯。8. 总结一套可以一直用的解题模板最后给你一个浓缩版的蜗牛排序使用模板来自我个人做算法题时积累的习惯拿到二维数组螺旋遍历题目先在脑中过三件事。第一边界怎么收缩——是用四指针还是方向向量取决于题目是否需要从非标准起点出发。第二转向条件是什么——如果是标准矩形用边界比较如果是不规则区域用 visited 数组。第三终止条件是什么——四指针法是 top/bottom 和 left/right 交错方向向量法是访问次数达到 m * n。这三件事都清楚了再动手写代码基本可以一次通过。如果是机考或者面试现场时间紧张我建议直接背下四指针法的模版因为这个模版最稳健、最不容易出错而且空间复杂度更优。遇到进阶变题再用方向向量法生成新的变体。再说一个小技巧如果你发现自己“改了 A 边界B 方向就漏元素改了 B 方向A 边界又重复输出”那就说明你还没有把四个方向当成一个闭环来思考。蜗牛排序的正确心态是——每一步都在“取元素 收缩边界”而不是“先全部取完再收缩”。顺序错了怎么调都别扭。