news 2026/10/2 9:20:23

螺旋矩阵 II 边界控制详解:循环不变量与按层填充

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
螺旋矩阵 II 边界控制详解:循环不变量与按层填充

我第一次做 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 为例,我会写:核心难点是四条边区间不重叠;解法使用"左闭右开"循环不变量;最容易写错的是奇数中心忘记填。就这么三句话,过一个月再看到,我也能在几分钟内把代码重新默写出来。

这种留痕习惯对面试冲刺特别有用。你不需要在最后几天重新翻几百道题,只需要翻自己写的复盘笔记,用最短时间把核心难点重新过一遍,效果远比临时刷题好。至少对我来说,刷题这条路走到后面,靠的已经不是聪明,而是方法。

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

ADB 自动化测试实战:命令、Python 封装、元素定位与排错

1. 先搞懂 adb 是什么&#xff1a;它凭什么成为自动化测试的地基刚接触 adb 自动化测试的人&#xff0c;几乎都会经历同一个阶段&#xff1a;把adb devices敲进命令行&#xff0c;看到一串设备号&#xff0c;然后陷入沉默——这玩意儿到底能帮我干什么&#xff1f;我自己的经历…

作者头像 李华
网站建设 2026/10/2 9:20:00

Linux安装Chrome与依赖解决、离线部署及沙箱权限指南

最小化安装的Linux服务器上装Chrome&#xff0c;最典型的场面是这样的&#xff1a;wget下来一个几十兆的deb包&#xff0c;dpkg -i一把梭&#xff0c;屏幕上立刻刷出一屏"依赖关系问题使得 google-chrome-stable 的配置工作不能继续"&#xff0c;然后卡在那里&#x…

作者头像 李华
网站建设 2026/10/2 9:19:56

ReaxFF反应力场参数拟合完全指南:从量子化学数据到LAMMPS模拟

我第一次接触ReaxFF反应力场时&#xff0c;最崩溃的不是分子动力学跑不动&#xff0c;而是被“参数拟合”这四个字堵在原地。网上讲ReaxFF的资料不算少&#xff0c;但绝大多数默认你手里已经有了一套可用的力场参数&#xff0c;只管扔进LAMMPS去跑。等到自己真的需要拟合一套Re…

作者头像 李华
网站建设 2026/10/2 9:19:52

COLA框架实战:Java工程化落地DDD的架构工具箱

1. 这不是又一本讲DDD的书&#xff0c;而是一套能立刻上手改代码的架构工具箱你打开一个Spring Boot项目&#xff0c;看到Controller里塞了200行逻辑&#xff0c;Service层调用七八个Mapper&#xff0c;DTO和VO在包里像俄罗斯套娃一样层层嵌套&#xff0c;领域模型&#xff1f;…

作者头像 李华
网站建设 2026/10/2 9:19:45

Docker Swarm全生命周期管理:10个关键实践范例

大家刚开始接触 Docker Swarm 时&#xff0c;多半会围着 docker service create 和 docker service scale 这两个命令打转&#xff0c;觉得“能起服务、能扩副本”就算会用了。但做了一段时间运维以后你会发现&#xff0c;命令只是表面&#xff0c;真正决定集群生死的是更外…

作者头像 李华
网站建设 2026/10/2 9:19:24

Paperclip:本地AI工作流胶合层,React+Node.js直连Claude与OpenClaw

1. 项目概述&#xff1a;Paperclip 是什么&#xff0c;它解决的到底是什么问题&#xff1f; Paperclip 这个名字乍一听容易让人联想到办公用品——回形针。但放在当前技术语境下&#xff0c;尤其结合你提供的热搜词组合&#xff08;Node.js、React、OpenClaw、Claude&#xff0…

作者头像 李华