news 2026/9/8 21:11:26

螺旋矩阵(蜗牛排序)全解:两种解法与边界条件详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
螺旋矩阵(蜗牛排序)全解:两种解法与边界条件详解

刷 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 res

2.3 为什么第三、四步之前要加条件判断

这段代码里最关键、也最容易被忽略的,就是第三步和第四步开头的 if 判断。很多新手抄代码的时候会漏掉这两行,然后运行一两个测试用例发现没问题,直到碰上特殊形状的矩阵才炸锅。

我们来看一个真实的反例——单行矩阵,比如[[1, 2, 3, 4]]。初始化 top = 0,bottom = 0,left = 0,right = 3,进入循环后:

第一步把第一行从左到右全部遍历完,res 变成 [1, 2, 3, 4],然后 top++,top 变成 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 的变化。

初始状态:top=0, bottom=2, left=0, right=2,res=[]

第一轮循环:

  • 第一步遍历 top 行,col 从 0 到 2,依次取 matrix[0][0]=1, matrix[0][1]=2, matrix[0][2]=3,res=[1,2,3],然后 top 变为 1。
  • 第二步遍历 right 列,row 从 1 到 2,依次取 matrix[1][2]=6, matrix[2][2]=9,res=[1,2,3,6,9],然后 right 变为 1。
  • 第三步判断 top<=bottom(1<=2)成立,遍历 bottom 行,col 从 1 到 0,依次取 matrix[2][1]=8, matrix[2][0]=7,res=[1,2,3,6,9,8,7],然后 bottom 变为 1。
  • 第四步判断 left<=right(0<=1)成立,遍历 left 列,row 从 1 到 1,取 matrix[1][0]=4,res=[1,2,3,6,9,8,7,4],然后 left 变为 1。

第一轮循环结束,四个边界值分别是 top=1, bottom=1, left=1, right=1,检查循环条件 1<=1 且 1<=1,满足,继续第二轮。

第二轮循环:

  • 第一步遍历 top 行,col 从 1 到 1,取 matrix[1][1]=5,res=[1,2,3,6,9,8,7,4,5],top 变为 2。
  • 第二步遍历 right 列,row 从 2 到 1,range(2, 2) 为空,不执行,right 变为 0。
  • 第三步判断 top<=bottom(2<=1)不成立,跳过。
  • 第四步判断 left<=right(1<=0)不成立,跳过。

循环条件检查:top=2, bottom=1,2<=1 不成立,循环结束,返回 [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 res

3.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]],初始化 top=0, bottom=2, left=0, right=0。第一步会把第一列的第一个元素取出来,然后第二步也就是从上到下遍历这一列,取出 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 边界保护在遍历下边、左边之前检查 top<=bottom 和 left<=right
结果缺少拐角元素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 边界又重复输出”,那就说明你还没有把四个方向当成一个闭环来思考。蜗牛排序的正确心态是——每一步都在“取元素 + 收缩边界”,而不是“先全部取完再收缩”。顺序错了,怎么调都别扭。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 21:08:15

pot-desktop|跨平台划词翻译与 OCR 识别使用指南

pot-desktop&#xff5c;跨平台划词翻译与 OCR 识别使用指南 【免费下载链接】pot-desktop &#x1f308;一个跨平台的划词翻译和OCR软件 | A cross-platform software for text translation and recognition. 项目地址: https://gitcode.com/GitHub_Trending/po/pot-desktop…

作者头像 李华
网站建设 2026/9/8 21:08:13

Python烟花动画的粒子系统实现与渲染优化

简介&#xff1a;本资源是一套基于Python实现的跨年动态烟花特效源码及配套素材&#xff0c;面向Python初学者与视觉编程爱好者&#xff0c;解决节日氛围营造、图形动画实践及GUI交互开发等实际需求。压缩包共26个文件&#xff0c;含1个核心py脚本&#xff08;实现烟花粒子系统…

作者头像 李华
网站建设 2026/9/8 21:04:33

开源终端AI编程助手opencode实战:安装配置、skills与插件生态全解析

最近几天我把主力终端AI助手从Claude Code切换到了opencode&#xff0c;中间踩了几个典型的坑&#xff0c;也发现了一些被低估的地方。如果你正在关注开源AI编程助手&#xff0c;想找一个能接多种模型、又有完整插件生态的工具&#xff0c;这篇是我用opencode接手实际开发工作的…

作者头像 李华