我第一次做 LeetCode 59. 螺旋矩阵 II 的时候,第一反应是去写一个"方向数组",上下左右四个方向,碰到边界就转向。结果 n=3 跑得挺好,n=4 一落地就越界,调了十分钟才意识到:这道题如果只跟着感觉走,很容易被边界条件反复摩擦。它挂在"数组"这个标签下,看起来只是生成一个二维数组,实际上考的是你对循环不变量、区间开闭和索引更新的掌控力。
LeetCode 这道题的价值恰恰在于:代码量不大,但是每一步都在试探你写循环的稳定性。很多人在刷 LeetCode 热门 100 题时会跳过它,觉得"不就是个螺旋嘛",可真到了面试手写环节,能一次写对的人并不多。这篇文章我会从手推边界开始,逐步拆解按层填充的标准写法,再把我实际调试中遇到过的坑和复盘方式完整分享出来。无论你是刚接触数组题的新手,还是准备秋招需要快速扫题的人,这篇文章都应该能帮你把这道题吃透。
1. 螺旋矩阵 II 的题目表面与隐藏考点
1.1 题干、输出与输入约束
题目很短:给你一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针螺旋顺序排列的 n x n 矩阵。例如:
- 输入 n = 3,输出:
[ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]- 输入 n = 1,输出:
[[1]]这个问题的输入约束只有一个 n,没有额外干扰项,所以解题的核心完全落在"如何控制螺旋的转向"以及"什么时候该往里缩一圈"上。
我第一次看到这个题的第一反应是模拟"走迷宫":在矩阵里放一个指针,当前方向向右,走不动了换方向。这种思路当然可行,但实现起来要考虑的边界状态特别多:指针越界、已经填过的格子不能重复填、每走一步都要判断是不是该转向。用这种思路做读取版的螺旋矩阵(LeetCode 54)还可以,但做生成版的 59,我更推荐另一种更贴近数组本质的思路——按层填充。
1.2 这道题真正在考查什么
表面上看,59 考的是"能不能按螺旋顺序填数"。往深了说,它考的是三件事。
第一,你能不能把二维数组的坐标系搞明白。很多同学一写二维数组就晕,分不清 matrix[i][j] 里谁是行、谁是列。其实你只要记住一个约定:i 控制行,也就是纵坐标;j 控制列,也就是横坐标。后面所有边界判断都基于这个约定,就不容易乱。
第二,你能不能保证一段循环里的"区间"从头到尾是一致的。这是最重要的隐藏考点。面试官看这道题的代码,看的就是你有没有一个明确的循环不变量——所谓循环不变量,就是你在每一轮循环里处理的是同一个区间规则,而不是这里写左闭右开,那里写左闭右闭,最后自己把自己绕晕。
第三,你能不能处理好"层数"和"中心点"的数学关系。n x n 矩阵有 n/2 层环,如果 n 是奇数,最中间会剩一个单独的元素。这个关系用到的是整除和余数的基本功,恰恰是很多人在紧张状态下容易漏掉的。
所以,这道题看起来是数组题,本质是一道"边界控制"的基础题。它不是靠记代码就能过关的题,而是需要你形成一套可迁移的循环思考方式。搞懂了这一题,后面你遇到二维数组的模拟类问题都会轻松很多。
2. 从 n=4 开始手推边界:为什么要讲"左闭右开"
2.1 手推 n=4 的过程:边界每一步怎么缩
直接上结论之前,我建议你先拿纸画一个 4x4 的格子,亲手把数字 1 到 16 按螺旋顺序填进去。你画完会发现顺序是这样的:
- 第 1 行从左到右:1, 2, 3, 4
- 最右列从上到下:5, 6, 7
- 最下行从右到左:8, 9, 10
- 最左列从下到上:11, 12, 13
- 然后进入内层,继续:14, 15, 16
注意看:外圈填了 12 个数,内圈填了 4 个数。外圈结束后,起点从 (0,0) 变成了 (1,1),同时可用的边界范围从整个 4x4 缩小成了一个 2x2 的区域。
| 步骤 | 方向 | 经过位置 | 边界变化 |
|---|---|---|---|
| 1 | 从左到右 | (0,0) -> (0,3) | 上边界下移 |
| 2 | 从上到下 | (1,3) -> (3,3) | 右边界左移 |
| 3 | 从右到左 | (3,2) -> (3,0) | 下边界上移 |
| 4 | 从下到上 | (2,0) -> (1,0) | 左边界右移 |
| 5 | 从左到右 | (1,1) -> (1,2) | 上边界再次下移 |
| 6 | 从上到下 | (2,2) -> (2,2) | 完成 |
这张表看下来,你应该能发现一个节奏:每走完一个方向,对应的那一条"墙"就往里缩一格。而这种"缩墙"的动作,就是按层填充法的全部秘密。
2.2 为什么要保持同一个循环不变量
很多人写螺旋矩阵会翻车,翻车的原因不是不会写循环,而是没有定义清楚"每一段循环管到哪里为止"。
以 n=4 为例,你要处理的第一轮是:
- 第一行:从第 0 列到第 3 列
- 最后一列:从第 1 行到第 3 行
- 最后一行:从第 2 列到第 0 列
- 第一列:从第 2 行到第 1 行
发现没有?四条边并不是都包含四个点。第一条边包含 4 个点,后面三条边都只包含 3 个点。如果你四条边都写"从起点到终点,包含两端",那么四个角会被重复填两次,后填的数字把先填的数字覆盖掉,整个矩阵就乱了。
这就是循环不变量要解决的问题。我选择的循环不变量是:每一条边都只处理"除了终点以外的部分",把终点留给下一条边处理。专业一点的说法叫"左闭右开"。第一条边填第 0 行第 0 列到第 3 列,但第 3 列那个格子不填,留给第二条边;第二条边再从第 1 行开始往下填;以此类推。这样每个角点都恰好被一条边处理一次,既不重复,也不遗漏。
2.3 左闭右开 vs 左闭右闭的选择
有些解法里也有人用"左闭右闭",就是每条边都把起点和终点都算进去,但同时要在每条边开始时略过拐角。这种写法不是不行,只是你需要额外判断"这次是不是第一条边"“这次是不是最后一条边”,逻辑分支明显变多。
我个人更推荐左闭右开,理由有三个:
- 符合循环习惯。写
for (j = start; j < n - offset; j++)这种条件,一眼就能看出区间范围,后续想调边界时不容易改错。 - 拐角天然交给下一条边,不需要额外处理重叠。
- 在调试打印矩阵时,每个格子只会被赋值一次,你可以放心地在赋值语句上加断点,不用担心因为覆盖导致数据对不上。
当然,如果你已经习惯了顶部、底部、左、右四条边界依次收缩的写法,那也是一种"闭区间"思路,我在后面的 JavaScript 示例里会给出完整写法。重要的是你选了一种规则,就要把它从头用到尾,不混用。
3. 按层填充的标准实现与完整代码
3.1 变量们的任务分配
先看按层填充的核心思路:每轮循环填外圈一圈,填完之后,起点往里挪一格,可用的边界也往里缩一格。重复这个过程,直到所有层都填完。
我以 Python 为例,说明几个关键变量的用途:
start_x和start_y:每轮循环的起始位置。第一轮是 (0,0),第二轮是 (1,1),第三轮是 (2,2)。因为矩阵是正方形,所以起点始终在对角线上,使用两个变量只是为了让行和列的更新更清晰。offset:表示当前这一轮要缩到哪。它并不是简单的边界值,而是配合"左闭右开"使用的一个偏移量。n - offset就是当前轮的不可到达边界。每完成一轮,offset加 1。loop:要循环多少轮。正方形矩阵的环数是n // 2。比如 n=4 是 2 轮,n=5 也是 2 轮,因为最后中间那个格子单独处理。num:当前要填入的数字,从 1 开始,每填一个格子就加 1,直到填满 n^2。
为什么循环轮数是n // 2?因为每一轮都会把矩阵的上下左右各往内缩一格,也就是同时消掉两行两列。对于 n x n 的矩阵,能完整消掉的行数是 n/2,剩下的如果不是整数,就说明中间还有一个孤立的格子没填。
文字推演看多了容易乱,我给一个 n=4 的调试过程,你可以对着代码看:
- 第一轮:起点在 (0,0),offset 为 1,有效边界是 n-1=3。第一步从左到右填 (0,0)(0,1)(0,2),第二步从上到下填 (1,3)(2,3)(3,3),第三步从右到左填 (3,2)(3,1)(3,0),第四步从下到上填 (2,0)(1,0)。
- 第二轮:起点在 (1,1),offset 为 2,有效边界是 n-2=2。第一步从左到右填 (1,1),第二步从上到下填 (2,2),第三步从右到左填 (2,2)。等等,这里你可能会发现第二步和第三步会撞车。这不是代码错了,而是第二轮里 2x2 的区域本质上只有四个点,用四条边去填一个 2x2 的圈,每条边只填一个点,四个点恰好覆盖完,所以第二步第三步填的是不同格子:第二步从上到下填 (2,2),第三步从右到左填 (2,2) 还是会重复。别急,看代码里第三步的起始位置,实际上是
(n - offset, start_y + 1)起的,所以在 2x2 场景下第三步只会填 (2,1),第四步填 (1,1),四个点刚好是 (1,1)(2,1)(2,2)(1,2)。
这里就体现出为什么说"左闭右开"在边界条件多的时候更稳。它在 2x2 这种极端情况下,依然能保证每个方向只负责该负责的点,不会因为边界判断多出重复赋值。
3.2 Python 示例
from typing import List def generateMatrix(n: int) -> List[List[int]]: # 初始化 n x n 的二维数组 matrix = [[0] * n for _ in range(n)] num = 1 start_x = start_y = 0 offset = 1 # 每轮向右、向下、向左、向上收缩的量 loop = n // 2 # 总共需要填的圈数 while loop: i = start_x j = start_y # 第一条边:从左到右 for j in range(start_y, n - offset): matrix[start_x][j] = num num += 1 # 第二条边:从上到下 for i in range(start_x, n - offset): matrix[i][n - offset] = num num += 1 # 第三条边:从右到左 for j in range(n - offset, start_y, -1): matrix[n - offset][j] = num num += 1 # 第四条边:从下到上 for i in range(n - offset, start_x, -1): matrix[i][start_y] = num num += 1 # 进入内层 start_x += 1 start_y += 1 offset += 1 loop -= 1 # 如果 n 是奇数,中心元素单独填入 if n % 2 == 1: matrix[start_x][start_y] = num return matrix这段代码里最关键的部分是四个 for 循环,它们的区间我完整列出来:
- 从上到下时,行范围是
range(start_x, n - offset),列固定是n - offset。 - 从下到上时,行范围是
range(n - offset, start_x, -1),列固定是start_y。
这四个循环连起来,刚好走一个完整的闭环,且四条边的首尾不重叠。你只要保证循环里的区间写法始终保持start到n - offset的这个"左闭右开"规则,任何规模的 n 都能套进去。
3.3 JavaScript 示例与二维数组初始化
很多人用 JavaScript 刷 LeetCode,我也顺手写一版。JS 里最容易出问题的就是二维数组初始化,我在这里会先讲清楚一个常见坑:如果你用new Array(n).fill(new Array(n)),fill 进去的是同一个数组引用,你改一行,所有行都会跟着变。这是刚接触 JS 数组的人很容易踩进去的陷阱。
正确写法是Array.from({ length: n }, () => new Array(n).fill(0)),这样每一行都是独立的新数组。这叫"先建行,再逐行填充",本质上和matrix = [[0]*n for _ in range(n)]是一个道理,都是为了避免共享引用。
用四条边界收缩的写法也很直观,适合高手快速过题:
/** * @param {number} n * @return {number[][]} */ var generateMatrix = function(n) { const matrix = Array.from({ length: n }, () => new Array(n).fill(0)); let num = 1; let top = 0, bottom = n - 1; let left = 0, right = n - 1; while (top <= bottom && left <= right) { // 从左到右填 top 行 for (let j = left; j <= right; j++) { matrix[top][j] = num++; } top++; // 从上到下填 right 列 for (let i = top; i <= bottom; i++) { matrix[i][right] = num++; } right--; // 从右到左填 bottom 行,注意防止行越界 if (top <= bottom) { for (let j = right; j >= left; j--) { matrix[bottom][j] = num++; } bottom--; } // 从下到上填 left 列,注意防止列越界 if (left <= right) { for (let i = bottom; i >= top; i--) { matrix[i][left] = num++; } left++; } } return matrix; };这段 JS 的思路和 Python 略有不同。Python 版本依赖 offset 统一收缩,JS 版本依赖四个独立的边界值。两者没有绝对优劣,但我这里要提醒你:如果在面试时写 JS 版本,那两个if (top <= bottom)和if (left <= right)是防越界的关键。因为当只有一行或只有一列时,你不需要再从右到左或从下到上填一遍,否则会覆盖已经填好的数据。
其他语言的写法大同小异。整体思路都是"四个方向 + 四种边界更新",只是语法习惯不同。你在纸上画出 n=3 的矩阵,用任何一种语言按这个思路推一遍,用不了几分钟就能跑通。
3.4 复杂度分析
这道题的时间复杂度是 O(n^2)。因为矩阵有 n^2 个格子,每个格子恰好被赋值一次,所以不管你用哪种写法,赋值次数都是 n^2。
空间复杂度要看你怎么算。如果只考虑额外空间,除了返回的矩阵之外,你只用了几个常数变量,比如 loop、offset、start_x,那额外的空间复杂度是 O(1)。但是因为题目要求返回 n x n 的矩阵,最终输出本身就要占 O(n^2) 的空间,所以大多数题解会写 O(n^2)。如果你在面试中被问到,建议先说清楚:实现过程只用常数辅助空间,但结果矩阵本身是 O(n^2)。
很多人会纠结"是不是有更快的算法",其实这个题的下限就是 O(n^2),因为你至少要往每个格子里写一个数,不可能少于这个量级。所以别在时间复杂度上浪费时间,把精力放到边界正确性和代码可读性上才是关键。
4. 我实际踩过的边界坑:四类典型错误
4.1 区间不一致导致元素覆盖
这个坑我在 2.2 里提过,但还是要单独拿出来强调,因为它是我见过的最高频错误。有同学写着写着,第一个循环用j <= right,第二个循环用i < bottom,等于混用了闭区间和开区间,最后螺旋到角落时,同一个格子被赋值两次,后面的数字把前面的覆盖了。
你可以用一个最简单的 n=3 来验证自己有没有写对:跑完代码后,检查矩阵每行每列是否刚好是 1 到 9 的一次排列。如果出现重复数字或者有 0,几乎可以肯定是区间边界写错了。
我的建议是,写代码之前先在注释里写清楚循环不变量。比如:
# 每轮四条边: # 边1:行固定为 start_x,列从 start_y 到 n-offset-1 # 边2:列固定为 n-offset,行从 start_x 到 n-offset-1 # 边3:行固定为 n-offset,列从 n-offset 到 start_y+1 # 边4:列固定为 start_y,行从 n-offset 到 start_x+1你写注释的过程中,就会提前发现边界问题,比写完整段代码再调试快得多。
| 错误类型 | 错误表现 | 修复方式 |
|---|---|---|
| 区间混用 | 角落被重复覆盖 | 统一定义开闭规则 |
| 忘记奇数中心 | n=5 时中间是 0 | 补if (n % 2 == 1) |
| 变量更新遗漏 | 走完第一层后永远填同一层 | 在循环末尾更新 start 和 offset |
| JS fill 引用共享 | 改一行全部行都变 | 使用 Array.from 逐行创建 |
表里这个"变量更新遗漏"是我后来在给同事 code review 时发现的。他第一层填得完美,但是loop递减了,start_x和offset却忘了更新,导致代码一直在填外圈,内圈全是 0。这道题如果 n 是偶数,你只看结果甚至会误以为正确,因为所有格子都有值,只是顺序错了。所以调试时一定要打印矩阵,不能只看有没有填满。
4.2 奇数 n 时中心元素被忽略
n=4 不会遇到这个问题,但是 n=5 会。5 的n // 2等于 2,也就是只用 while 循环填两圈。两圈填完之后,矩阵正中心剩下一个格子,它的坐标恰好是(start_x, start_y),此时start_x和start_y都已经加到了 2。
如果你漏了if (n % 2 == 1)这个判断,那个格子就永远是初始值 0,你的答案就会少最后一个数字。我自己的经验是,这个坑在第一次提交时非常容易踩到,尤其是当你拿 n=4 测试通过以后,会形成一种"我的代码没问题"的错觉,然后换 n=5 直接翻车。
一个很实用的自测习惯是:每次提交之前,至少跑三个用例,n=1、n=2、n=5。n=1 测最简边界,n=2 测最小闭环,n=5 测奇数中心。这三个用例能覆盖 90% 的边界错误。
4.3 把循环变量当成静态值使用
这个坑具体表现为:在四个 for 循环里复用同一个变量名,而且忘记在进入下一层之前重新赋值。比如你在循环外面定义了 i 和 j,第一层填完以后,i 和 j 停在某个角落位置,下一层继续使用,导致起点偏差。
避免方法很简单:每一轮 while 循环开始前,重新给 i 和 j 赋值为start_x和start_y。我在示例代码里就是这么写的:
i = start_x j = start_y你可能会觉得这是小事,但实际在紧张的笔试环境里,这种细节最容易引发"明明思路对,代码却跑不过"的诡异问题。我的个人习惯是把循环变量局部化,能定义在 for 循环内的就定义在内部,不反复复用同一个变量名,减少状态残留。
4.4 JS 里用 fill 初始化二维数组的坑
这个不算螺旋矩阵逻辑本身的坑,而是数组题里最常见的二维数组初始化问题。我第一次用 JS 写矩阵题时,使用了new Array(n).fill(new Array(n).fill(0)),结果给第一行第三个格子赋值时,每一行第三个格子都变了。
原因是fill方法传入的是同一个数组对象,new Array(n).fill(0)只执行了一次,生成了一个数组,然后这个数组被 n 个位置共享。之后你对其中任意一行的修改,都会同步反映到所有行上。
正确做法我已经写在第 3.3 节的代码里了。用Array.from({ length: n }, () => new Array(n).fill(0)),每次回调都会创建一个全新的数组。Python 里也有类似的坑吗?有的,如果你写[[0] * n] * n,外层* n同样会发生引用共享。所以必须用列表推导式[[0] * n for _ in range(n)]才对。
这个坑足够隐蔽,也是数组热词里"数组初始化"被反复搜索的原因之一。我建议你直接在本地记一份笔记:二维数组初始化,Python 用列表推导式,JavaScript 用 Array.from 加箭头函数,不要在初始化这里图省事。
4.5 调试方法:打印矩阵
最后分享一个我一直在用的调试技巧。刷这种二维数组题,别只用眼睛看代码,直接在关键位置打印矩阵,费不了几分钟。
Python 里可以这样写一个简单的输出:
def print_matrix(matrix): for row in matrix: print(" ".join(str(x).rjust(2) for x in row))在每轮 while 循环结束后调用print_matrix(matrix),你会直观地看到每一层是怎样被填满的,也能第一时间发现哪个方向填错了、哪个格子被漏掉了。
我实测下来,这个方法解决边界问题时比断点调试更好用。因为你关注的不是某个变量的值,而是整个矩阵的形态变化。看到第一层填完后第二层起点在哪儿、方向对不对,心里就有谱了。
5. 不止一种做法:从 59 延伸到 54 和方向模拟
5.1 和 54 螺旋矩阵的互逆关系
LeetCode 里还有一道和 59 对应的经典题:54 螺旋矩阵。59 是"给你 n,生成螺旋矩阵",54 是"给你一个 m x n 的矩阵,按螺旋顺序读出来"。
这两题思路是同源的,但 59 实现起来通常更简单,因为正方形的四条边天然对称,而 54 是 m x n,有可能行和列不相等,边界判断会麻烦一点。
我的建议是先把 59 吃透,再去做 54。做过 59 之后,你再去 54 里使用四边界法,写起来会顺手很多,因为你已经建立了"边界收缩"的心智模型,剩下的只是把"填数"改成"读数"。
"边界收缩"模型还有一个好处,就是它和旋转图像那一类题能共用一套思考框架。比如 48 旋转图像,要求原地旋转 90 度,本质上也是循环地处理"层",每一层做四个边的轮转。你有了按层处理矩阵的经验,再看这类题会觉得豁然开朗。
5.2 方向数组模拟法
方向数组模拟法是另一种常见解法。思路是维护一个当前位置和一个方向索引,每走一步就判断下一步是否越界或者是否已经访问过,如果满足条件就转向。
这种解法用一个 moves 数组代表四个方向的增量:
const dirs = [ [0, 1], // 向右 [1, 0], // 向下 [0, -1], // 向左 [-1, 0] // 向上 ];每走一步,先计算下一步的位置,如果下一步超出边界或者目标位置已经有值,就把方向索引加 1 对 4 取模。这种方法适合 54,因为未知尺寸的矩阵需要动态判断;也适合 59,只是需要额外多检查一次 visited 数组。
我不太推荐在 59 上用方向数组模拟法,不是因为不对,而是因为这题是正方形,按层填充可以省掉很多重复判断,代码也更简洁。但如果你是想练习方向数组的思路,用 59 当练手题是完全没问题的。两种解法都写一遍,你对矩阵问题的理解会明显上一个台阶。
5.3 递归逐层法
还有一种是递归逐层法:每一层递归处理一个环,然后把问题规模缩小到内部子矩阵。递归的终止条件是 n <= 0 或者 n === 1。
这种写法的优点是逻辑特别清晰,每层递归只负责自己的四条边,不用担心 while 循环里的各类变量更新。缺点是递归调用有额外栈开销,而且对于 n=1000 这种非常大的输入,可能会出现栈溢出风险。LeetCode 的常规输入规模不会大到这种程度,所以递归法作为备选方案是完全可以的,但面试时我更推荐用迭代法,因为更稳。
递归法的代码框架大概是这样的思想:传入当前轮的起点和边长,如果边长为 1,填中心后返回;先填外圈四条边,然后递归处理边长减 2 的内部矩阵。你不需要在代码里维护复杂的 offset 和 loop,递归参数本身就承载了层数信息,容易写对,也容易讲清楚。
5.4 延伸到旋转图像与搜索类题目
59 教给你的循环不变量和边界收缩能力,可以平移到很多数组题上。
- 54 螺旋矩阵:把"填数"变成"读数",难度略升。
- 48 旋转图像:按层轮转四个角,边界处理类似,但需要原地交换,对索引要求更高。
- 74 搜索二维矩阵 / 240 搜索二维矩阵 II:把二维数组当一维数组处理,或者利用行有序列有序的特性,边界思路完全不同,但底层还是数组索引的精确控制。
- 498 对角线遍历:同样是模拟类题目,需要你维护坐标状态,反向类比螺旋的转向逻辑。
有同学问我,刷题到底是刷数量还是刷思路?我的回答是,刷题刷到一定程度,真正让你记住的不是题目本身,而是题目背后的模式。59 的模式就是"用循环不变量处理二维数组的层状结构"。你把这个模式提取出来,迁移到任何同类型题目上都会很快。
6. 复盘方法:怎么把这道题变成可复用的边界能力
6.1 三步复盘法:输入、状态、更新
我刷这类边界题之后,会专门花十分钟做一次复盘,复盘框架固定为三步:输入、状态、更新。
- 输入是什么:n 的值,以及矩阵初始化形态。
- 每一轮有哪些状态:起点坐标、圈数、偏移量、当前数字。
- 每一轮怎么更新:填完之后起点加一,偏移量加一,圈数减一,数字累加。
只要你能在三句话之内把这三点讲清楚,就说明你真的掌握了。面试官问你原理时,你就按这个顺序答,他会觉得你是真的在思考,而不是背模板。
这个三步法不仅适用于螺旋矩阵,也适用于很多数组题。你拿到一道新的数组题,先问自己:我维护的状态是什么?这个状态在每轮怎么变?变化条件是什么?想清楚这个,基本就把题目解了一半。
6.2 用最小边界用例验证代码
我在 4.2 里说过,要测 n=1、n=2、n=5。这里再展开讲一下为什么是这三个值。
- n=1 是最小边界,没有外圈,直接进入中心填充逻辑。如果中心判断写错,这里会马上暴露。它只有一格,非常适合作为第一轮调试。
- n=2 是最小的闭环,四条边每条只有一个格子。这个用例能帮你验证四个方向的"左闭右开"是否完全正确,任何一个边界写错,都会出现覆盖或者越界。
- n=5 是奇数且大于 2,能够同时验证多层循环和中心填充,是覆盖最全面的用例。
当然了,正常笔试时你不会只提交一次。我的习惯是本地写好一个测试函数,直接循环从 1 到 8 全部跑一遍,每个 n 都验证生成的矩阵里数字是否是从 1 到 n^2 的完整排列且互不重复。这种批量验证比我手动看几次结果要快得多。
6.3 刷这类题的心态与"留痕"
最后说点个人体会。我见过太多人刷题打卡,一天刷十道,但过两周回头看,一道都不记得。原因就是刷的时候没有"留痕"。
我说的留痕不是复制粘贴题解,而是用自己的话在题解旁边写三行注释:这一题的核心难点是什么?我的解法用了什么循环不变量?我在哪里最容易写错?你写下了这些,下次遇到类似题目就能很快提取出记忆,因为这是你自己的语言体系和思考路径。
以 59 为例,我会写:核心难点是四条边区间不重叠;解法使用"左闭右开"循环不变量;最容易写错的是奇数中心忘记填。就这么三句话,过一个月再看到,我也能在几分钟内把代码重新默写出来。
这种留痕习惯对面试冲刺特别有用。你不需要在最后几天重新翻几百道题,只需要翻自己写的复盘笔记,用最短时间把核心难点重新过一遍,效果远比临时刷题好。至少对我来说,刷题这条路走到后面,靠的已经不是聪明,而是方法。