ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

蛇形矩阵详解:二维数组、方向控制在矩阵填充类算法中的应用

蛇形矩阵详解:二维数组、方向控制在矩阵填充类算法中的应用 在群里答应过要把经典数组题一个个拆开讲透蛇形矩阵正好排到第二篇。很多初学者第一次看到这个题第一反应是“这不就是矩阵填数嘛”真动起手来不是方向搞反就是边界写错。我拿这道题当过不少回面试热身题也帮别人改过好几版代码发现它的考察点其实非常集中二维数组的下标操作、循环边界控制、方向切换逻辑。这篇文章就围绕这几个点展开给出可复现的完整代码再把蛇形矩阵和它的近亲——螺旋矩阵、之字形遍历——放在一起对比让你看完之后不只是会背一种写法而是真正理解这一类题的通解思路。1. 蛇形矩阵到底在考什么题目本质与典型形态先明确题目定义。蛇形矩阵最常见的形态是一个 n 行 n 列的方阵从左上角开始按“奇数行从左到右、偶数行从右到左”的顺序填入 1 到 n² 的自然数。以 n5 为例结果长这样1 2 3 4 5 10 9 8 7 6 11 12 13 14 15 20 19 18 17 16 21 22 23 24 25一眼看上去像一条蛇在矩阵里来回游走所以叫蛇形矩阵。也有一些教材或在线判题系统会把螺旋矩阵也叫“蛇形矩阵”这属于命名不统一后面我会专门把两者的区别讲清楚。本文默认讨论的是这种“行与行之间反向”的经典蛇形矩阵。1.1 题目变种的常见设定除了方阵有的题会要求输出 m 行 n 列的矩形蛇形矩阵填数规则完全一样只是行数和列数可以不等。还有的题要求按从 1 开始递增填充但蛇形方向是从右到左起手也就是第一行倒着填。这些都属于同一个题干的微调核心算法不变变的是奇偶判断的基准和循环的起始位置。实际做题时我建议先确认三件事起点是左上还是右上、第一行是正序还是倒序、行号从 0 开始还是从 1 开始。这三个细节一旦确认代码就不会出现方向性错误。1.2 这道题考察的三个核心能力第一二维数组的遍历顺序。很多人能轻松写出两层 for 循环但一旦遇到“这一行从右往左”就不知道内层循环怎么控制列下标了。第二奇偶行的逻辑判断。蛇形矩阵的规律是行号决定填充方向这里必须理清如果行号从 0 开始则偶数行正序、奇数行倒序如果行号从 1 开始则奇数行正序、偶数行倒序。第三循环不变量意识。每填一个数计数器加一每遍历完一行进入下一行。这个流程看起来简单但在边界条件下比如 n1容易出问题。我见过不少候选人能给出正确代码但问一句“为什么奇数行要从右往左”就卡住了。其实答案就藏在题目定义里相邻两行方向必须相反这样才能形成蛇形路径。理解了这一点哪怕题目改成“每三行换一次方向”或者“按对角线蛇形填充”你也知道怎么应付。2. 正向解法模拟填数路径的方向控制法最直观的思路就是完全按照蛇形路径的行走方向一个格子一个格子地去填。这种解法贴近自然语言描述不需要找数学规律初学者很容易接受。2.1 核心思想把矩阵当成一张有边界的地图想象你手里有一张 n×n 的地图初始位置在左上角手里握着一个计数器从 1 开始。规则只有一条先向右走走到右边界就向下走一行同时调转方向改为向左走走到左边界再向下走一行再调转方向改为向右走。如此循环。这个描述里最关键的词是“调转方向”对应到代码里就是依靠行号的奇偶来判断当前行的填充方向。用代码实现这个逻辑时不需要真的设计一个“当前位置”的坐标因为蛇形矩阵的方向变化非常有规律同一行内方向不变行与行之间方向反转。所以直接用两层循环外层循环遍历行号 i内层循环遍历列号 j根据 i 的奇偶决定 j 的取值顺序。正序时第 j 列是第 j 个格子倒序时第 j 列对应矩阵里下标为 n-1-j 的那个格子。2.2 方向数组与边界判定的两种实现如果你以后还要做螺旋矩阵、迷宫问题我建议接触一下方向数组的写法。它把“向右、向下、向左、向上”抽象成 dx、dy 两个数组每次撞到边界就切换方向索引。蛇形矩阵虽然用不上四方向但这个思想可以简化理解# 方向向量右、下、左、上 dx [0, 1, 0, -1] dy [1, 0, -1, 0]不过蛇形矩阵只有左右两个方向用方向数组反而绕。更直接的做法是def snake_matrix(n): matrix [[0] * n for _ in range(n)] num 1 for i in range(n): if i % 2 0: # 偶数行从0起从左到右 for j in range(n): matrix[i][j] num num 1 else: # 奇数行从右到左 for j in range(n - 1, -1, -1): matrix[i][j] num num 1 return matrix这个版本的循环条件非常直白range(n) 表示 0 到 n-1range(n-1, -1, -1) 表示 n-1 到 0。每次内层循环结束后 num 恰好增加了 n正好进入下一行。这里不用手动改变 num因为填数是一个连续递增的过程。2.3 每一步的状态更新与循环终止条件外层循环 i 的范围是 0 到 n-1共 n 行。每次进入新的一行num 的当前值就是上一步结束时“下一个待填的数字”内层循环把它依次写入。当外层循环结束后num 应该等于 n² 1此时所有格子都已经填完。这里有一个容易被忽略的点判断奇偶用的是行下标 i而不是行号。如果题目描述里说“第 1 行从左到右第 2 行从右到左”那么这里的逻辑就是“当 i 为偶数时第 1 行正序当 i 为奇数时第 2 行倒序”。有些同学把范围写成 range(1, n1)然后判断的时候用了 i % 2 1结果也一样但建议统一用 0 基下标省得来回换算。提示内层循环的 range 写法要小心。range(n-1, -1, -1) 中间那个 -1 表示左边界如果写成 range(n-1, 0, -1)会漏掉第 0 列。这个错误属于“差一错误”测试时很难发现因为结果看起来只是最后一列少了数字。3. 反向解法直接用下标公式推算每个位置的数第二种思路完全不模拟填数路径而是直接找规律每个位置 (i, j) 上应该填的数能不能用一个公式算出来如果可以那就连内层循环里的判断都不用做了。3.1 观察行首数字的规律先看每一行的开头数字1, 10, 11, 20, 21。第一行开头是 1第二行开头是 10第三行开头是 11。再结合每行有 n 个数这个事实可以发现第 i 行的起始数字是 i * n 1。以 n5 为例第 0 行起始 0511第 1 行起始 1516不对第二行开头是 10。等一下这里要小心。观察实际输出第一行是 1 2 3 4 5第二行是 10 9 8 7 6。如果把每一行都看成一个整体那么整个矩阵其实是按行顺序填充的先填 1 到 5再填 6 到 10只不过第二行的 6 到 10 被反向排列了。所以第 i 行包含的原始数字范围是 i*n1 到 (i1)*n这一点无论方向如何都成立。区别只在于偶数行从 0 起原始顺序从左到右所以 matrix[i][j] i*n j 1奇数行从 0 起原始顺序从右到左所以 matrix[i][j] in (n - j) 推一下原始数字 in 1 到 (i1)n反向排列后第 j 列对应原始顺序里的第 n-j 个数字从 1 开始数因此 matrix[i][j] in n - j。验证 i1, j0得 15 5 - 0 10正确j4得 1*55-46正确。这个公式用 Python 写出来非常简洁def snake_matrix_by_formula(n): matrix [[0] * n for _ in range(n)] for i in range(n): for j in range(n): if i % 2 0: matrix[i][j] i * n j 1 else: matrix[i][j] (i 1) * n - j return matrix3.2 公式法的通用版本如果题目要求矩形矩阵m 行 n 列公式改为行满列时每行有 n 个数第 i 行原始范围是 i*n1 到 (i1)n偶数行 matrix[i][j] in j 1奇数行 matrix[i][j] (i1)*n - j。这个推广不需要改任何逻辑只把外层循环的上限从 n 改成 m 即可。def snake_matrix_rect(m, n): matrix [[0] * n for _ in range(m)] for i in range(m): for j in range(n): if i % 2 0: matrix[i][j] i * n j 1 else: matrix[i][j] (i 1) * n - j return matrix3.3 两种解法的对比与选型对比维度模拟填数法公式推导法理解难度直观符合直觉需要先找规律代码量稍长稍短运行时间O(n²)O(n²)扩展性容易扩展到螺旋矩阵遇到复杂路径推导困难出错点边界、奇偶判断下标换算我的建议是刚接触题目用模拟法因为它能帮你建立“路径感”如果是在笔试环境里追求快速且不易错公式法更稳。但两者都必须掌握因为很多变体题是以两者为基础的。4. 多语言落地Python、Java、C 的完整实现思路讲清楚了接下来把三种主流语言的完整代码都摆出来。这些代码我都跑过直接复制就能出结果。4.1 Python 版本最省事的写法def snake_matrix(n): matrix [[0] * n for _ in range(n)] num 1 for i in range(n): if i % 2 0: for j in range(n): matrix[i][j] num num 1 else: for j in range(n - 1, -1, -1): matrix[i][j] num num 1 return matrix def print_matrix(matrix): for row in matrix: print( .join(f{x:2d} for x in row)) if __name__ __main__: n int(input(请输入 n: )) print_matrix(snake_matrix(n))这段代码里用到了列表推导式 matrix [[0] * n for _ in range(n)]很多人会写错成 [[0] * n] * n。后者虽然语法没错但每一行都是同一个对象的引用修改任意一个元素会影响整列这是 Python 二维数组初始化最经典的坑。4.2 Java 版本二维数组的默认值陷阱import java.util.Scanner; public class SnakeMatrix { public static int[][] snakeMatrix(int n) { int[][] matrix new int[n][n]; int num 1; for (int i 0; i n; i) { if (i % 2 0) { for (int j 0; j n; j) { matrix[i][j] num; } } else { for (int j n - 1; j 0; j--) { matrix[i][j] num; } } } return matrix; } public static void printMatrix(int[][] matrix) { for (int[] row : matrix) { for (int val : row) { System.out.printf(%2d , val); } System.out.println(); } } public static void main(String[] args) { try (Scanner scanner new Scanner(System.in)) { int n scanner.nextInt(); printMatrix(snakeMatrix(n)); } } }Java 的 int 数组默认值就是 0不需要手动初始化填充这点比 Python 方便。但要注意 System.out.printf 的格式化缺乏对齐会让输出看起来像乱码尤其是 n 较大的时候。4.3 C 版本vector 与变长数组#include iostream #include vector #include iomanip using namespace std; vectorvectorint snakeMatrix(int n) { vectorvectorint matrix(n, vectorint(n)); int num 1; for (int i 0; i n; i) { if (i % 2 0) { for (int j 0; j n; j) { matrix[i][j] num; } } else { for (int j n - 1; j 0; j--) { matrix[i][j] num; } } } return matrix; } void printMatrix(const vectorvectorint matrix) { for (const auto row : matrix) { for (int val : row) { cout setw(2) val ; } cout endl; } } int main() { int n; cin n; printMatrix(snakeMatrix(n)); return 0; }C 这里建议一律使用 vectorvector 而不是传统的 int matrix[n][n]。原因有两个一是变长数组在 C 标准里本就不被正式支持部分编译器做了扩展二是 vector 不需要手动管理内存返回时也更安全。这里 i % 2 0 判断的效率没有问题现代编译器会把它优化成位运算不存在性能担忧。4.4 三种语言的换行与输出格式差异同样的逻辑三种语言跑出来的结果一致但输出格式略有差异。Python 的 f-string、Java 的 printf、C 的 setw 都用来控制对齐。实际遇到判题系统时一般只要求数字序列正确不要求空格数完全匹配但本地调试时最好统一成两位对齐看起来舒坦。注意在 Java 和 C 里num 是标准写法注意不要写成 num 然后顺手改其他逻辑。这种细节在 Python 里不存在因为 Python 没有自增运算符。跨语言迁移代码时必须留意这些差异。5. 从蛇形矩阵延伸出去的变体家族蛇形矩阵是“矩阵填充”这一大家族的入口弄懂了它螺旋矩阵、之字形遍历、对角线填充都不再是死记硬背的题目。5.1 螺旋矩阵蛇形从行反转变成方向循环螺旋矩阵的填充路径是向右走到底向下走到底向左走到底向上走到底然后缩小范围继续。它和蛇形矩阵最大的区别是蛇形只在左右两个方向之间切换螺旋则按顺时针循环四个方向。def spiral_matrix(n): matrix [[0] * n for _ in range(n)] top, bottom, left, right 0, n - 1, 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 判断奇数尺寸的矩阵会多填一行或一列。你可以拿 n4 和 n5 各跑一遍感受区别。5.2 之字形遍历行内方向交替的更复杂版本之字形遍历和蛇形矩阵很像区别在于它不一定需要填充整个矩阵而是按路径输出已有数组的元素。比如对给定的二维数组按以下顺序输出第一行从左到右第二行从右到左第三行从左到右……这种遍历方式是图像扫描、锯齿形读取的常见操作。实现和蛇形矩阵几乎一样只是从“填数”变成“读数”。5.3 实际场景中的蛇形逻辑不要觉得蛇形矩阵只是算法练习。图像处理里有 zigzag 扫描JPEG 编码处理 8×8 块时用到数据存储里有蛇形分布的布局优化游戏开发地图生成里也有类似路径。理解了这种模式以后遇到“如何按 S 形遍历一个二维空间”的问题你会自然地想到同样的思路。6. 调试心得与易错点清单写代码和调代码是两回事。下面这些坑我在实际帮人改代码的过程中遇到过列出来供你对照排查。6.1 最常翻车的三个点第一二维数组初始化错误。Python 里 [[0]*n]*n 是同一个行对象的拷贝填了一行结果整列都跟着变。第二倒序循环的 range 边界写错。第三奇偶判断基准不统一一会儿从 0 起、一会儿从 1 起输出整个错位。6.2 多组测试数据的验证方法建议至少用 n1、2、3、5 四组数据验证。n1 验证边界时要确保循环不会越界n2 能检查奇偶逻辑n5 能检查大一点的对齐和完整性。每次输出后数一下最大值是不是 n²以及矩阵里的数是否正好是 1 到 n² 各出现一次。6.3 个人实践的几点体会我做过很多数组类题目的代码走查蛇形矩阵这道题最大的价值不是它本身而是它强迫你建立“二维空间中有方向地移动”的思维方式。一旦你开始用方向数组、边界变量来思考问题后面的螺旋矩阵、BFS 网格遍历、迷宫路径都会顺手很多。建议你学完这篇文章后不要满足于复制代码手动在纸上画一个 4×4 的格子模拟每填入一个数字时 i、j 的变化体验一次“人肉调试”。最近我再刷一遍这些基础题发现用公式法写蛇形矩阵越来越顺手已经变成我写笔试代码时的默认方案。但模拟法我也保留了因为遇到复杂的变体题目时模拟法的路径感能帮我快速定位逻辑错误。两种方法没有高下之分关键在于你理解到哪一层。
返回列表