bigywater.tech
算法与数据结构 · LeetCode 59

LeetCode 59. 螺旋矩阵 II

给定正整数 n,生成一个 n × n 的矩阵,把 1 到 n² 按顺时针螺旋的顺序依次填进去。这是第 54 题"螺旋矩阵"的反向:那一题是按螺旋顺序读出,这一题是按螺旋顺序写入。同样可以用四条边界一圈一圈往里收缩。本文实测两种不同的实现(逐圈填充与"方向加碰壁右转"的模拟),并且如实报告一个我事先预期会出问题、结果却没有出问题的地方:上一题里需要的"至少 2 行 2 列才走下边和左边"的判断,在方阵上其实可以省略。给出五种语言的实现,并把逐圈填充做成分步演示。

30
组随机对拍,与暴力解的结果全部一致
9 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。基准解用另一种思路:一个当前位置和一个方向,每填一个数就沿当前方向前进一格,碰到边界或者已经填过的格子就右转,与逐圈收缩边界是两种独立的写法。一共 10 组用例,n 取 1 到 9 和 20:

真实实测 · 本机执行输出
输入 [1]
输出 [[1]]
输入 [2]
输出 [[1,2],[4,3]]
输入 [3]
输出 [[1,2,3],[8,9,4],[7,6,5]]
输入 [4]
输出 [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
输入 [5]
输出 [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
输入 [6]
输出 [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]

一个没有出问题的"陷阱"

在第 54 题里,如果每一圈无条件地走四条边,遇到只有一行或一列的区域就会重复走格子。我预期这一题奇数 n 时最后一圈只剩中心一个格子,无条件走下边和左边会把中心格子重复填。下面是我真的把这个错误写法跑了一遍的结果,同时统计它一共填了多少个数:

真实实测 · 本机执行输出
# 无条件走下边、左边的写法:n 为奇数时,最后一圈只剩一个格子
n=1: 应该填 1 个数,该写法填了 1 个,结果 [[1]]    solve -> [[1]]
n=2: 应该填 4 个数,该写法填了 4 个,结果 [[1, 2], [4, 3]]    solve -> [[1, 2], [4, 3]]
n=3: 应该填 9 个数,该写法填了 9 个,结果 [[1, 2, 3], [8, 9, 4], [7, 6, 5]]    solve -> [[1, 2, 3], [8, 9, 4], [7, 6, 5]]
n=5: 应该填 25 个数,该写法填了 25 个,结果 [[1, 2, 3, 4, 5], [16, 17, 18, 19, 6], [15, 24, 25, 20, 7], [14, 23, 22, 21, 8], [13, 12, 11, 10, 9]]    solve -> [[1, 2, 3, 4, 5], [16, 17, 18, 19, 6], [15, 24, 25, 20, 7], [14, 23, 22, 21, 8], [13, 12, 11, 10, 9]]

结果:这种写法在 n = 1、2、3、5 上填的个数都正好等于 n²,结果与 solve 完全一样,没有任何错误。原因是:方阵每一圈收缩后总是要么还是一个方阵,要么最后一圈只剩一个格子(奇数 n);只剩一个格子时,下边的范围(从 right - 1 走到 left)和左边的范围(从 bottom - 1 走到 top + 1)都是空范围,循环体一次也不执行,所以根本不会重复填。也就是说,方阵情形下"至少 2 行 2 列"的判断是多余的,而非方阵(上一题)才需要它。本文的解法仍然保留了这个判断,它没有害处,并且与上一题保持一致。这个结论只针对方阵,我只验证了 n = 1、2、3、5,其他 n 是通过随机对拍间接验证的。

逐圈填充:五种语言的实现

top、bottom、left、right 是还没填过的正方形区域,num 是下一个要填的数。每一圈依次填上边(第 top 行,从左到右)、右边(第 right 列,从上到下)、下边(第 bottom 行,从右到左)、左边(第 left 列,从下到上),四条边各向里收缩一格。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(n): mat = [[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): # 上边,从左到右 mat[top][j] = num num += 1 for i in range(top + 1, bottom + 1): # 右边,从上到下 mat[i][right] = num num += 1 if top < bottom and left < right: # 至少 2 行 2 列时才有下边和左边 for j in range(right - 1, left - 1, -1): # 下边,从右到左 mat[bottom][j] = num num += 1 for i in range(bottom - 1, top, -1): # 左边,从下到上 mat[i][left] = num num += 1 top, bottom, left, right = top + 1, bottom - 1, left + 1, right - 1 return mat
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[1]  →  [[1]]
[2]  →  [[1,2],[4,3]]
[3]  →  [[1,2,3],[8,9,4],[7,6,5]]
[4]  →  [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
[5]  →  [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
[6]  →  [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]
Java · 解法代码class Solution { public int[][] generateMatrix(int n) { int[][] mat = new int[n][n]; int top = 0, bottom = n - 1, left = 0, right = n - 1, num = 1; while (top <= bottom && left <= right) { for (int j = left; j <= right; j++) mat[top][j] = num++; // 上边,从左到右 for (int i = top + 1; i <= bottom; i++) mat[i][right] = num++; // 右边,从上到下 if (top < bottom && left < right) { // 至少 2 行 2 列时才有下边和左边 for (int j = right - 1; j >= left; j--) mat[bottom][j] = num++; // 下边,从右到左 for (int i = bottom - 1; i > top; i--) mat[i][left] = num++; // 左边,从下到上 } top++; bottom--; left++; right--; } return mat; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[1]  →  [[1]]
[2]  →  [[1,2],[4,3]]
[3]  →  [[1,2,3],[8,9,4],[7,6,5]]
[4]  →  [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
[5]  →  [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
[6]  →  [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]
Go · 解法代码func generateMatrix(n int) [][]int { mat := make([][]int, n) for i := range mat { mat[i] = make([]int, n) } top, bottom, left, right, num := 0, n-1, 0, n-1, 1 for top <= bottom && left <= right { for j := left; j <= right; j++ { // 上边,从左到右 mat[top][j] = num num++ } for i := top + 1; i <= bottom; i++ { // 右边,从上到下 mat[i][right] = num num++ } if top < bottom && left < right { // 至少 2 行 2 列时才有下边和左边 for j := right - 1; j >= left; j-- { // 下边,从右到左 mat[bottom][j] = num num++ } for i := bottom - 1; i > top; i-- { // 左边,从下到上 mat[i][left] = num num++ } } top, bottom, left, right = top+1, bottom-1, left+1, right-1 } return mat }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[1]  →  [[1]]
[2]  →  [[1,2],[4,3]]
[3]  →  [[1,2,3],[8,9,4],[7,6,5]]
[4]  →  [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
[5]  →  [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
[6]  →  [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]
JavaScript · 解法代码function generateMatrix(n) { const mat = Array.from({ length: n }, () => new Array(n).fill(0)); let top = 0, bottom = n - 1, left = 0, right = n - 1, num = 1; while (top <= bottom && left <= right) { for (let j = left; j <= right; j++) mat[top][j] = num++; // 上边,从左到右 for (let i = top + 1; i <= bottom; i++) mat[i][right] = num++; // 右边,从上到下 if (top < bottom && left < right) { // 至少 2 行 2 列时才有下边和左边 for (let j = right - 1; j >= left; j--) mat[bottom][j] = num++; // 下边,从右到左 for (let i = bottom - 1; i > top; i--) mat[i][left] = num++; // 左边,从下到上 } top++; bottom--; left++; right--; } return mat; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[1]  →  [[1]]
[2]  →  [[1,2],[4,3]]
[3]  →  [[1,2,3],[8,9,4],[7,6,5]]
[4]  →  [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
[5]  →  [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
[6]  →  [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]
C++ · 解法代码#include <vector> using namespace std; class Solution { public: vector<vector<int>> generateMatrix(int n) { vector<vector<int>> mat(n, vector<int>(n, 0)); int top = 0, bottom = n - 1, left = 0, right = n - 1, num = 1; while (top <= bottom && left <= right) { for (int j = left; j <= right; j++) mat[top][j] = num++; // 上边,从左到右 for (int i = top + 1; i <= bottom; i++) mat[i][right] = num++; // 右边,从上到下 if (top < bottom && left < right) { // 至少 2 行 2 列时才有下边和左边 for (int j = right - 1; j >= left; j--) mat[bottom][j] = num++; // 下边,从右到左 for (int i = bottom - 1; i > top; i--) mat[i][left] = num++; // 左边,从下到上 } top++; bottom--; left++; right--; } return mat; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[1]  →  [[1]]
[2]  →  [[1,2],[4,3]]
[3]  →  [[1,2,3],[8,9,4],[7,6,5]]
[4]  →  [[1,2,3,4],[12,13,14,5],[11,16,15,6],[10,9,8,7]]
[5]  →  [[1,2,3,4,5],[16,17,18,19,6],[15,24,25,20,7],[14,23,22,21,8],[13,12,11,10,9]]
[6]  →  [[1,2,3,4,5,6],[20,21,22,23,24,7],[19,32,33,34,25,8],[18,31,36,35,26,9],[17,30,29,28,27,10],[16,15,14,13,12,11]]
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

耗时实测

n 分别为 100、300、1000、2000(n² 个元素),每个规模取三次中最快的一次,基准是方向模拟:

真实实测 · 本机执行输出
n | 暴力解 | 本文解法
       100 |     0.961 ms | 0.279 ms
       300 |     8.338 ms | 2.712 ms
      1000 |   105.088 ms | 42.217 ms
      2000 |   500.309 ms | 236.115 ms

两种做法都是 O(n²),n = 2000 时(400 万个元素)逐圈填充用了 236.1ms,方向模拟用了 500.3ms,约 2.1 倍:方向模拟每步要做边界和已填检查,逐圈填充直接连续地填一条边。逐圈填充的耗时从 100 到 300(元素 9 倍)增长约 9.7 倍,从 300 到 1000(元素 11 倍)增长约 15.6 倍,从 1000 到 2000(元素 4 倍)增长约 5.6 倍,比线性略高,我没有细究,可能与大矩阵的缓存行为有关,这只是猜测。

随机对拍

Python 版本另外和方向模拟做了随机对拍,n 取 1 到 30:

真实实测 · 本机执行输出
随机对拍 30 组(n 取 1 到 30 的随机值),solve 与 brute 的结果全部一致。

n 只有 30 种取值,固定用例和对拍合起来覆盖了大部分。另外四种语言只在上面 10 组固定用例上验证过(n 取 1 到 9 和 20)。

分步演示:一圈一圈填

下面的演示用 n = 4:第一圈走外面一圈的 12 个格子,第二圈是中间的 2×2。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终矩阵与 solve 的返回值一致。第二圈的"上边""右边""下边"各只填一个格子,最后一步之后左边的范围为空。

螺旋矩阵 II:n = 4,把 1 到 16 按顺时针螺旋顺序填进去
刚填入的格子已填的格子

    参考与说明

    • 官方题目页:LeetCode 59. 螺旋矩阵 II。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:为什么方阵可以省略判断的严格证明;本文只用小规模实验和随机对拍验证。
    京ICP备2026067502号-1京公网安备11010502063487号