回型矩阵螺旋矩阵算法详解:按层模拟与方向向量法实战

发布时间:2026/10/15 22:59:10
回型矩阵螺旋矩阵算法详解:按层模拟与方向向量法实战
回型矩阵也叫螺旋矩阵是算法入门路上绕不开的一道板子题。它做的事情很简单在一个二维数组里按顺时针一圈圈走要么把数字从 1 到 n² 按顺序填进去要么把已经填好的数组按这个顺序读出来。第一次接触的人往往觉得逻辑很清晰可一旦动手写就会在边界条件上反复翻车。这篇文章就把回型矩阵的思路彻底拆开讲清楚按层模拟为什么是最稳的写法方向向量法适合什么场景以及那些“看着没问题、一跑就死循环”的坑到底出在哪。适合正在学算法、准备面试或者比赛前想背模板的同学参考。你可以把它当成一块自测二维数组功底的地板砖踩明白了很多数组题都会顺畅不少。1. 回型矩阵到底在考什么先分清楚两种题型1.1 填充型把 1 到 n² 按螺旋塞进矩阵最常见的形式是给定一个正整数 n要求生成一个 n 行 n 列的矩阵数字从 1 开始顺时针逐层递增。n3 时结果长这样1 2 3 8 9 4 7 6 5n4 时1 2 3 4 12 13 14 5 11 16 15 6 10 9 8 7可以看到整个过程像是沿着矩阵的外圈画一个顺时针的“回”字走完一圈之后往里缩一圈。这个“往里缩一圈”就是整个题目的核心。这类题本身并不依赖高深的数据结构难点全在坐标控制和循环边界。比如从第一行的 (0,0) 走到 (0,n-1)然后掉头向下再到 (n-1,n-1)再往左回到 (n-1,0)再往上去到 (1,0)紧接着开始第二圈。里面每一段要走多少个元素什么时候停很多新手就是在这里开始混乱的。如果你能把填充型的坐标轨迹在纸上完整画出来后面所有变体基本都难不倒你。1.2 读取型按螺旋顺序把矩阵元素读出来另一种常见形态是反过来给一个已经存在的二维矩阵要求把它的元素按顺时针螺旋顺序输出。这种题型通常给的是 m 行 n 列的矩形不一定是正方形。比如 3 行 4 列的矩阵输出路径同样是先右、再下、再左、再上不断向内收缩直到所有元素被读完。读取型和填充型本质上是一件事填的时候你是按螺旋顺序给每个坐标赋值读的时候你是按螺旋顺序把每个坐标上的值取出来。坐标移动的规律完全一样所以边界控制的方法也完全可以复用。不过读取型有个额外要求结果中不能有重复元素也不能漏掉一整个区域。这个要求在矩形矩阵中尤其容易被破坏后面我讲按层模拟时会重点说明。1.3 为什么叫“板子题”值得特意去背在竞赛圈里“板子题”指的是那种解法固定、没什么技巧变化的模板题。回型矩阵就是最典型的板子之一考的就是背下模板、控制好边界。它不需要动态规划不需要什么复杂的数据结构但很考验代码的严谨程度。很多人口述思路头头是道一写代码就漏个边界或者出现重复覆盖。所以花一点时间把回型矩阵的板子彻底吃透对任何人来说都很划算。这里我建议先掌握“按层模拟”再把“方向向量法”作为备用。按层模拟更适合填充型方向向量法更适合读取型。两套思路本质一样只是表达方式不同。接下来就分别讲这两种方法。2. 最稳的按层模拟法边界和填充顺序一次讲透2.1 四边界模型像剥洋葱一样收缩按层模拟法的核心是维护四个变量分别表示当前这圈的上下左右边界。我习惯用 top、bottom、left、right 来命名。初始时top0bottomn-1left0rightn-1。每一圈分四条边处理上边在 top 这一行从 left 到 right逐个填数字。填完这一行说明最上面这一行已经用完了top 加 1。右边在 right 这一列从 top 到 bottom逐个填数字。填完这一列说明最右边这一列也用完了right 减 1。下边在 bottom 这一行从 right 到 left逐个填数字。填完这一行bottom 减 1。左边在 left 这一列从 bottom 到 top逐个填数字。填完这一列left 加 1。每填完一条边对应的边界就朝内收缩一格。类比一下这个过程很像剥洋葱最外圈剥掉剩下一个更小的圈继续用同样的方式处理。为什么要每走完一条边就立刻更新边界而不是四条边走完再一起更新因为后续边的起点和终点依赖前一条边更新后的边界。如果你等到一圈都走完才更新第二次循环时使用的是同一个未收缩的边界就必然会把已经填过的区域再走一遍要么死循环要么重复覆盖。这个细节是新手最容易踩的坑没有之一。还有一个小问题为什么用 top、bottom、left、right 四个变量而不是直接算“当前是第几层”因为四个变量可以让每一条边的起点终点都只和变量相关逻辑非常统一。如果只记一个层数 k那么每条边的起点终点还得根据 k 临时计算容易出错。四个变量是空间换易读性的典型做法代价只有四个整数非常划算。2.2 可直接背的 Python 板子下面这个模板我用了很多年稍微改一改就能应付填充型题目。以生成 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这段代码里最需要留意的是下边和左边之前的两个 if 判断。处理 n 阶方阵时这两个判断有时看起来是多余的但一旦题目变成 m 行 n 列的矩形少了这两个判断就会出大问题。比如只剩一行的时候上边已经处理完了下边如果继续执行就会把同一行反过来再填一遍覆盖掉刚才的数字。所以我把这两个判断直接写在板子里作为默认防护。有同学会问右边处理完之后right 已经减了 1这时候下边循环用 range(right, left-1, -1) 是否可能越界不会因为外层 while 保证了 top bottom 且 left right这一层至少还有一格存在。加上前面的 if 判断即使出现了只剩一列或者一行的极端情况也只会走正确的边。如果你用 C 写逻辑完全一样vectorvectorint generateMatrix(int n) { vectorvectorint matrix(n, vectorint(n, 0)); int top 0, bottom n - 1, left 0, right n - 1; int num 1; while (top bottom left right) { for (int j left; j right; j) matrix[top][j] num; top; for (int i top; i bottom; i) matrix[i][right] num; --right; if (top bottom) { for (int j right; j left; --j) matrix[bottom][j] num; --bottom; } if (left right) { for (int i bottom; i top; --i) matrix[i][left] num; left; } } return matrix; }两种语言的思路一模一样唯一要注意的是 Python 的 range 不包含结束位置C 的循环包含结束位置写反了就会出现差一错误。2.3 奇数 n 的中心元素为什么不会漏当 n 是奇数时最内圈只剩下一个格子。举个例子n5 走到最后一圈时top、bottom、left、right 全部等于 2。此时 while 条件仍然成立进入循环上边循环会填 matrix[2][2]num 增加。top 变成 3此时 top 已经大于 bottom。右边循环 range(top, bottom1) 等价于 range(3, 3)空循环不会执行。right 减 1 变成 1。下边循环之前先判断 top bottom此时 3 2 不成立跳过。左边循环之前先判断 left right此时 2 1 不成立跳过。left 加 1 变成 3while 条件退出。这样一来最中心的那个格子被上边循环填掉了且不会被重复执行。这就是为什么外层用 while top bottom and left right而不是用 while top bottom。如果条件写成小于号奇数阶矩阵的中心格子就会整块漏掉最后矩阵里留下一个 0。偶数 n 的情况则会在最后一层剩下一个 2x2 的小圈四条边都能正常执行也不会多填。所以“while 条件 if 防护”这套组合就是处理奇偶边界的关键。时间复杂度方面填充型需要给 n² 个位置各赋值一次所以是 O(n²)。额外只用几个整数边界变量空间复杂度 O(1)。这个复杂度很优秀面试时可以直接说。3. 方向向量法不用记边界的另一种板子3.1 用方向数组加 visited 数组控制转向按层模拟法的优点是快、省空间但需要理清四条边的起点终点。如果你不习惯画图也可以换一种思路让“当前位置”自己走碰到边界或者走过的格子就右转。方向向量法的核心是两个东西一个方向数组一个 visited 数组。方向数组固定为“右、下、左、上”四个方向例如右 (0, 1) 下 (1, 0) 左 (0, -1) 上 (-1, 0)当前坐标 r, c 每次先尝试朝当前方向走一步得到新坐标 nr, nc。如果新坐标越界或者那个位置已经访问过了就说明该转向了把方向索引取下一项重新计算 nr, nc。然后移动到新坐标继续循环。这个方法的核心逻辑是“判断能不能走不能走就转向”而不是预先划分每一条边。它天然适配任意矩形也天然适配读取型题目因为你不需要知道当前是哪一层只需要判断当前位置的下一个位置是否合法。从实现难度上说方向向量法的代码往往比按层模拟短而且不容易漏中心。代价是要额外开一个 visited 数组空间上略逊一筹。不过在笔试和面试中空间 O(m*n) 通常是可以接受的所以它也是我非常推荐的备用板子。3.2 读取型通用模板与原理下面给一个读取型通用模板输入任意 m 行 n 列的矩阵按顺时针螺旋顺序返回一维数组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)] d 0 r, c 0, 0 res [] for _ in range(m * n): res.append(matrix[r][c]) visited[r][c] True nr r directions[d][0] nc c directions[d][1] if nr 0 or nr m or nc 0 or nc n or visited[nr][nc]: d (d 1) % 4 nr r directions[d][0] nc c directions[d][1] r, c nr, nc return res这段代码每次循环只处理一个格子所以循环次数正好等于矩阵元素总数 m*n。visited 数组的作用是防止走到已经访问过的格子方向是“右、下、左、上”循环切换但如果没有 visited你会在第 2 圈的时候走进上一圈填过的区域。这里的空间复杂度是 O(m*n)因为多开了一个同等大小的 visited 数组。很多地方要求你写“原地”算法能否省掉 visited对于方阵可以借助按层模拟来原地完成对于任意矩阵如果允许修改原数组可以把访问过的元素改成特殊值作为标记但这样破坏了原数据不推荐。所以方向向量法更适合作为读取型题目的默认做法用一点空间换清晰度。3.3 按层法和方向法怎么选两种方法我都在实际中用简单总结一下选择依据对比维度按层模拟法方向向量法核心原理维护四条边界逐层收缩方向数组 visited 标记代码风格四个循环 边界更新单循环 越界/重复判断空间复杂度O(1)O(m*n)正方形填充非常合适也能做但需 visited矩形读取改 if 判断后能用天然适配出错概率边界顺序记错容易崩相对不容易漏中心面试侧重点考察边界控制能力考察模拟过程理解我的建议是填充型题目优先用按层模拟读取型题目优先用方向向量法。如果你只想背一个板子那就背按层模拟如果担心自己总是忘记边界更新那就把方向向量法练熟。两套都掌握当然最好因为它们本质上是同一个“螺旋移动”过程的两种表达。4. 实战中的错误与调试技巧这些坑我全踩过4.1 高频翻车现场汇总回型矩阵的代码不长但错误类型非常集中。我整理了几个出现频率最高的问题行列变量混用把 matrix[i][j] 写成 matrix[j][i]尤其是用 i、j 命名坐标时。回型矩阵里“行”和“列”交替使用一旦变量名没有语义就很容易在四条边的循环中搞反。建议统一用 row、col或者干脆用 r、c明确 r 是行、c 是列。边界更新时机错乱有人习惯在每一条边处理完之后不更新边界而是在一圈结束之后统一更新。结果第二轮循环还会沿用上一轮的 top、left导致越界或重复赋值。少了 if 防护在只剩一行或一列时下边或左边的循环重复处理了已经填充过的位置。对于 n 阶方阵这个错误发生在最后一圈只剩一列或一行的时候可能表现为中心格子被覆盖成更大的数字。while 条件写错写成 top bottom 或 left right导致奇数阶矩阵的中心元素没有被填充或者写成 top bottom and left right 的取反逻辑导致死循环。空矩阵没判读取型题目输入可能是空数组或 [[]]没有边界判断就直接访问 matrix[0][0]直接越界报错。这些错误有个共同点单独看每一条都很简单但叠加在一起就非常让人抓狂。所以调试时要针对边界打点不要盯着中间数据看。4.2 三个有效的调试习惯第一个习惯是先在纸上画出矩阵然后沿着代码走一遍。不用画大矩阵n3 或 n4 就够。把每一步的 top、bottom、left、right 值记下来对照着看循环是否越界这是最直观的方法。第二个习惯是在代码里临时打印边界变量和坐标。比如按层模拟里在每个循环开始前加上print(top, top, bottom, bottom, left, left, right, right, num, num)输出出来之后你会很清楚地看到边界是怎么收缩的也能立刻发现是哪一步跳出了预期。实际调试时我经常发现某轮循环里 right 突然比 left 小了那一定是前面某条边的更新写反了。第三个习惯是写一个验证函数生成矩阵后用集合判断所有数字是否恰好是 1 到 n²且没有重复。对于读取型则判断结果长度是否等于 m*n。这样即使小规模代码没有报错也能检查逻辑正确性。用简单的断言就能实现def check(matrix, n): flat [x for row in matrix for x in row] assert len(flat) n * n assert sorted(flat) list(range(1, n * n 1))写这种验证函数不会花很多时间却能帮你快速定位问题省下的调试时间远超这个成本。4.3 板子默写训练法我给很多同学推荐过一个笨办法把按层模拟模板默写五遍不看书不查资料。第一遍可能卡在 left、right 的先后顺序上第二遍会卡在 if 防护上到第三遍就会形成肌肉记忆。关键是默写的时候要默念每一步的原因为什么先走 top为什么走完立刻 top为什么下边前要判断 top bottom这样默写出来的不是代码而是边界变化的流程。我在刷题阶段专门花了一个下午干这件事。从 n2、n3、n5 开始手动推再跑代码验证。过了这一关之后再遇到读取型、矩形矩阵、逆时针这类变体都只是在这个板子上做小改动而已。5. 变体扩展从回型矩阵到螺旋面试题全家桶5.1 改成逆时针、从右下角出发很多题目不会老老实实让你顺时针从左上角开始比如要求逆时针填充或者从某个内圈开始。这时候不必重新发明算法只需要调整方向顺序。逆时针填充把四条边顺序从“上、右、下、左”改成“左、下、右、上”对应方向数组改为左、下、右、上。按层模拟里依然是四条边但边的顺序变了。从右下角开始顺时针向中心绕可以让数字从 n² 开始倒着填或者仍然从 1 开始但在坐标映射上做镜像。更简单的方法是先按正常方式生成再把矩阵整体旋转或翻转如果允许使用额外空间的话。给定一个坐标作为起点向外螺旋扩展这种题方向法通常比按层法好改因为只需要改变初始坐标和方向数组不需要重新推圈数。遇到变体时我建议先在草稿纸上画出路径然后回到基础模板里改方向数组或边的执行顺序不要从零开始另写一套。5.2 处理 m x n 的矩形矩阵读取型题目里矩阵通常不是正方形而是 m 行 n 列。按层模拟也可以继续用只是要特别注意两个 if 判断。当只剩一行时right 列已经走过left 列也可能已经走过没有 if 防护下边循环会把那一行再走一遍结果输出里出现重复元素。当只剩一列时左边循环又会把那一列反着再走一遍。所以如果你用的是我给的模板矩形场景下它天然是安全的。另一个注意点是 while 条件仍然用 top bottom and left right最后的“中心”不是一个格子而可能是一条直线。比如 2x5 的矩阵剥到某一层会只剩一行此时上边循环处理掉整行后面下边被 if 挡住程序正常退出。用方向向量法也能得到相同结果且代码更省心。所以矩形读取我更推荐方向法。5.3 从内圈往外、蛇形矩阵这些近亲回型矩阵还经常和“蛇形矩阵”混在一起。蛇形矩阵通常指第一行从左到右第二行从右到左第三行再从左到右这样一行一行地翻转方向不需要一圈圈剥洋葱。它虽然也叫“蛇形”但和回型矩阵不是一回事。遇到蛇形矩阵时不要套回型的板子直接按行方向取反即可。从内圈往外螺旋填数则可以把回型的逻辑反过来先从中心开始按逆序从大到小填充。一个取巧的做法是先生成正常的顺时针从外到内的矩阵再把矩阵做中心对称数字大小关系也会自然反过来。如果题目要求的是从内圈到外圈的螺旋输出则可以从内圈开始或不断收缩最内层的边界但换汤不换药。5.4 掌握板子的底层规律遇到新题也能改回型矩阵虽然只是“板子题”但它考的是二维数组索引和边界控制的底层能力。面试时如果你能把回型矩阵讲清楚说明你对数组下标、循环不变式、空间复杂度这些基础概念有扎实的理解。我的体会是不要死记代码要记住“边界收缩”和“转向判断”这两个核心思想。前者对应按层模拟后者对应方向向量法。看到任何螺旋类题目先问自己三个问题起点在哪方向顺序是什么什么时候转向或收缩想清楚这三个问题模板怎么改心里就有数了。说实话回型矩阵这道板子题代码加起来不到 30 行但它常年出现在基础算法训练和面试里就是因为它能一针见血地暴露你对边界条件的敏感度。我后来带新同学的时候也总是先让他们把这题写干净。如果你今天看完文章建议不要直接关掉找个在线编辑器把两种模板各自默写一遍再用 n3 和 4 验证一遍。尤其是那些 if 判断和边界更新一定要亲手踩一次坑印象远比看十遍深刻。等你把按层模拟和方向向量法都练成了肌肉记忆再遇到螺旋矩阵的变体你会发现自己根本不需要“想”手自然会动。