目录

题目描述

59. 螺旋矩阵 II

考察公司:小米

image-20230305150635551

题意分析

给定正整数 n,要求生成一个 n x n 的方阵,按顺时针螺旋的顺序把 1 依次填进去:从左上角出发向右,撞到边界后向下,再向左,再向上,一圈圈收拢到中心。

有两个约束值得单独拎出来。其一是矩阵必然是正方形,行数等于列数,因此不存在长条形矩阵那些「最后剩一行还是剩一列」的不对称情况——但代码仍要能处理收缩到只剩单行或单列的那一刻。其二是填入的数恰好是 1..n² 这个连续区间,一个不多、一个不少:这等价于说每个格子被写且只被写一次,写多了会覆盖已填的值,写少了会留下 0。这两条合起来把题目变成一道纯粹的「不重不漏」的边界控制题。

数据范围 1 <= n <= 20 很小,暗示不必考虑效率,考点全在写法的严谨性上。

边界情况:n = 1 时答案是 [[1]],只填一个格子,任何多填一次的写法都会在这里暴露;n 为奇数时最中心是一个孤立的格子,它既是「单行」也是「单列」;n 为偶数时最内层是一个 2 x 2 的方块,能被四条边正常走完。

解法:四边界螺旋填充

核心思路

问题关键:数字的路径是确定的,但每走完一圈,下一圈的起点和范围都会变化。用方向数组模拟「碰壁转向」需要额外判断是否访问过,状态更分散。

为什么选四边界:任意时刻,未填写区域都是一个矩形。用 topbottomleftright 表示它,按上、右、下、左依次填写四条边;每完成一条边,就把对应边界向内收缩一格。这与 54 题螺旋读取是同一骨架,只是把读取改成写入。

不变量:每轮开始时,边界矩形内的格子全部未填,矩形外的格子已经按顺时针顺序填好,value 是下一个要写入的数。四条边处理完后,未填区域仍是一个更小的矩形,因此不变量可以逐圈维持。

正确性:每条边只填写当前未填矩形的外沿,随后立即移出该边;因此不同轮次不会重复访问同一格。边界交叉时未填矩形为空,所有 个位置都已按路径依次写入 1..n²

收尾条件:上边、右边收缩后,矩形可能已经退化为空。填写下边前要检查 top <= bottom,填写左边前要检查 left <= right,否则单行或单列会被重复填写。

解题步骤

  1. 创建 n x n 矩阵,初始化四条边界和 value = 1
  2. 从左到右填写上边,随后 top++
  3. 从上到下填写右边,随后 right--
  4. 若仍有行,从右到左填写下边,随后 bottom--
  5. 若仍有列,从下到上填写左边,随后 left++
  6. 重复上述过程,直到上下或左右边界交叉。

口述样例n = 3 时,第一圈依次填入 1,2,3 | 4,5 | 6,7 | 8,边界收缩后只剩中心格,再填 9,得到 [[1,2,3],[8,9,4],[7,6,5]]

代码实现

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] matrix = new int[n][n];
        int top = 0;
        int bottom = n - 1;
        int left = 0;
        int right = n - 1;
        int value = 1;

        while (top <= bottom && left <= right) {
            for (int col = left; col <= right; col++) {
                matrix[top][col] = value++;
            }
            top++;
            for (int row = top; row <= bottom; row++) {
                matrix[row][right] = value++;
            }
            right--;
            if (top <= bottom) {
                for (int col = right; col >= left; col--) {
                    matrix[bottom][col] = value++;
                }
                bottom--;
            }
            if (left <= right) {
                // 左边界仍存在时,再从下到上填充左边。
                for (int row = bottom; row >= top; row--) {
                    matrix[row][left] = value++;
                }
                left++;
            }
        }

        return matrix;
    }
}
func generateMatrix(n int) [][]int {
    matrix := make([][]int, n)
    for i := 0; i < n; i++ {
        matrix[i] = make([]int, n)
    }

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

    for top <= bottom && left <= right {
        for col := left; col <= right; col++ {
            matrix[top][col] = value
            value++
        }
        top++
        for row := top; row <= bottom; row++ {
            matrix[row][right] = value
            value++
        }
        right--
        if top <= bottom {
            for col := right; col >= left; col-- {
                matrix[bottom][col] = value
                value++
            }
            bottom--
        }
        if left <= right {
            // 边界检查避免单行或单列时重复填充。
            for row := bottom; row >= top; row-- {
                matrix[row][left] = value
                value++
            }
            left++
        }
    }

    return matrix
}

复杂度分析

  • 时间复杂度:$O(n^2)$。每个格子恰好填写一次,且输出本身就有 个元素。
  • 空间复杂度:$O(1)$。除返回矩阵外只使用四条边界和一个计数器。

关键点总结

  • 四条边界描述的是「尚未处理的矩形」,每填一条边就立即收缩。
  • 遍历方向必须固定为右、下、左、上,四个角只由先到达的那条边填写。
  • 下边和左边开始前必须重新检查边界,处理单行、单列和中心格。
  • 证明不重不漏:每条已填边立即移出未填矩形,边界最终覆盖并移除全部 个格子。
  • 方向模拟也能完成,但需要转向和已填判断;四边界更适合面试现场书写。

易错点总结

  • 下边、左边不做二次边界判断:n = 1 时中心格会被重复覆盖。
  • 填右边仍从旧 top 开始:右上角会写两次;后续边同理要避开已处理的角。
  • 填完边后忘记收缩对应边界,会重复填写甚至死循环。
  • 下边、左边方向写反,会破坏顺时针次序;n = 3 是最小的有效检查样例。
  • Go 的二维切片只创建外层还不够,每一行都必须单独 make,否则写入时越界。

相似题目

题目 难度 考察点
54. 螺旋矩阵 中等 同一骨架的读取版,矩阵是 m x n 长方形,退化判断更易触发
剑指 Offer 29. 顺时针打印矩阵 简单 54 的等价题,需额外处理空矩阵输入
48. 旋转图像 中等 同样按层处理方阵,但要求原地四点轮换而非顺序填数
面试题 01.07. 旋转矩阵 中等 48 的等价题,可用转置加翻转替代按层轮换