59. 螺旋矩阵 II

LeetCode 原题链接

题目描述

给定一个正整数 n,生成一个包含 1 所有元素,且元素按顺时针螺旋顺序排列的 n × n 矩阵。

输入:n = 3
输出:
[
  [1, 2, 3],
  [8, 9, 4],
  [7, 6, 5]
]

题型判断

这是一道矩阵模拟题。数字的填写顺序固定为:

从左到右 → 从上到下 → 从右到左 → 从下到上

每填完最外面一圈,尚未填写的区域仍然是一个更小的正方形,因此可以维护四条边界:

  • top:当前未填写区域的上边界;
  • bottom:当前未填写区域的下边界;
  • left:当前未填写区域的左边界;
  • right:当前未填写区域的右边界。

完成一条边后,立即将对应边界向内收缩。这样每个格子恰好只会被填写一次。

核心思路:四边界收缩

初始化时,未填写区域是整个矩阵:

top = 0
bottom = n - 1
left = 0
right = n - 1

每一轮按照下面四个方向填写。

1. 填写上边:从左到右

for (let column = left; column <= right; column++) {
  matrix[top][column] = number++;
}
top++;

上边填写完成后,它不再属于未填写区域,所以执行 top++

2. 填写右边:从上到下

for (let row = top; row <= bottom; row++) {
  matrix[row][right] = number++;
}
right--;

这里从更新后的 top 开始,避免重复填写右上角。

3. 填写下边:从右到左

for (let column = right; column >= left; column--) {
  matrix[bottom][column] = number++;
}
bottom--;

这里从更新后的 right 开始,避免重复填写右下角。

4. 填写左边:从下到上

for (let row = bottom; row >= top; row--) {
  matrix[row][left] = number++;
}
left++;

这里使用更新后的 bottomtop,避免重复填写两个左侧角落。

为什么后两条边要检查边界

每填完一条边,未填写区域都会缩小。当矩阵收缩到只剩一行或一列时,前面的循环可能已经把最后的格子全部填完。

因此填写下边之前需要判断:

if (top <= bottom)

填写左边之前需要判断:

if (left <= right)

虽然本题输入一定是正方形,有些重复填写在结果上可能不容易暴露,但统一加上边界判断可以保证每个格子只访问一次,也能直接迁移到矩形螺旋遍历问题。

JavaScript 实现

/**
 * @param {number} n
 * @return {number[][]}
 */
var generateMatrix = function (n) {
  const matrix = Array.from({ length: n }, () => Array(n).fill(0));

  let top = 0;
  let bottom = n - 1;
  let left = 0;
  let right = n - 1;
  let number = 1;

  while (top <= bottom && left <= right) {
    // 上边:从左到右
    for (let column = left; column <= right; column++) {
      matrix[top][column] = number++;
    }
    top++;

    // 右边:从上到下
    for (let row = top; row <= bottom; row++) {
      matrix[row][right] = number++;
    }
    right--;

    // 下边:从右到左
    if (top <= bottom) {
      for (let column = right; column >= left; column--) {
        matrix[bottom][column] = number++;
      }
      bottom--;
    }

    // 左边:从下到上
    if (left <= right) {
      for (let row = bottom; row >= top; row--) {
        matrix[row][left] = number++;
      }
      left++;
    }
  }

  return matrix;
};

示例推演

n = 3 为例。

初始边界:

top = 0, bottom = 2
left = 0, right = 2
number = 1

第一轮填写最外层:

上边:1 2 3
右边:    4
下边:7 6 5
左边:8

此时矩阵为:

1 2 3
8 0 4
7 6 5

边界收缩为:

top = 1, bottom = 1
left = 1, right = 1

第二轮只剩中心格,填写 9

1 2 3
8 9 4
7 6 5

填写后 top > bottom,循环结束。

循环不变量

每轮 while 循环开始时始终满足:

  1. 边界外的格子已经按照顺时针顺序填写完成;
  2. [top, bottom] × [left, right] 是当前尚未填写的区域;
  3. number 是下一个应该写入的数字。

一轮依次填写当前区域的上、右、下、左四条边,然后收缩边界。新边界包围的区域仍然全部未填写,因此不变量继续成立。

循环结束时,至少有一组边界交错,说明未填写区域为空。由于数字从 1 开始,每写入一个格子后加一,所以最终矩阵恰好包含 1

复杂度分析

  • 时间复杂度:O(n²)。矩阵共有 个格子,每个格子恰好填写一次。
  • 辅助空间复杂度:O(1)。除返回矩阵外,只使用了常数个变量。
  • 返回结果空间:O(n²),用于存储生成的矩阵。

常见错误

1. 二维数组的每一行引用了同一个数组

不要这样初始化:

const matrix = new Array(n).fill(new Array(n).fill(0));

fill 会让所有行指向同一个数组,修改一个位置会影响多行。应为每一行分别创建数组:

const matrix = Array.from({ length: n }, () => Array(n).fill(0));

2. 填完一条边后没有立即收缩边界

如果四个方向都使用最初的边界,四个角会被重复填写。正确顺序是:

填写上边 → top++
填写右边 → right--
填写下边 → bottom--
填写左边 → left++

3. 混淆行和列

矩阵访问格式是:

matrix[row][column]
  • 水平移动:行不变,修改 column
  • 垂直移动:列不变,修改 row

4. 循环边界写成 <

四条边界 top、bottom、left、right 都表示有效下标,因此遍历时通常使用 <=>=。如果写成严格不等号,容易漏掉边界格子或中心格。

5. 单独处理奇数中心,但外层循环又填写了中心

按四边界写法时,while (top <= bottom && left <= right) 会自然处理奇数矩阵的中心格,不需要再写 n % 2 === 1 的特殊逻辑。

与「54. 螺旋矩阵」的关系

两道题使用相同的四边界模型:

题目操作
54. 螺旋矩阵按螺旋顺序读取已有矩阵
59. 螺旋矩阵 II按螺旋顺序向新矩阵写入数字

因此掌握下面这个方向模板后,两题只差“读取”还是“写入”:

左 → 右
上 → 下
右 → 左
下 → 上

另一种结束条件

因为一定需要填写 个数字,也可以使用数字作为循环条件:

while (number <= n * n) {
  // 按四个方向填写并收缩边界
}

这种写法同样可行,但四边界条件:

while (top <= bottom && left <= right)

更直接地表达了“只要还有未填写区域就继续”,也更容易复用到非正方形矩阵。

一句话总结

维护 top、bottom、left、right 四条边界,按照“上、右、下、左”的顺序填写当前外圈,每完成一条边就立即向内收缩对应边界,直到未填写区域为空。