题目描述

✅ 剑指 Offer 29. 顺时针打印矩阵

image-20261001230752552

image-20260928183828071

image-20260928183828073

题意分析

从矩阵左上角出发,按向右、向下、向左、向上的顺序沿外圈读取,再继续读取内圈,直到所有位置都访问一次。返回按访问顺序排列的一维数组,不需要修改矩阵。

矩阵不一定是方阵,可能为空,也可能只剩一行或一列。相同数值出现在不同位置时仍要分别输出,去重针对的是位置是否重复访问,而不是数值。

解法:四边界模拟

核心思路

[!blue]

每读完一圈,未访问的部分仍然是一个矩形,所以不必逐格记录是否访问。用闭区间 [top, bottom] 和 [left, right] 描述剩余矩形,每轮按顺时针方向剥去四条边。

先从左到右读取 top 行,然后 top++;右边从更新后的 top 开始向下读取,因此不会重复右上角。接着 right--,下边再从更新后的 right 向左读取,便不会重复右下角。同理,读完下边后 bottom--,左边从新的 bottom 向上读取,最后 left++。

收缩途中可能已经没有剩余行或列。读下边前需要确认 top <= bottom,读左边前需要确认 left <= right;另一方向的循环上下界则自然使已空区间不执行。这样最后只有一行或一列时,也不会反向再读一遍。

一轮结束后,四条边各自只处理一次,内层剩余矩形继续由四个边界完整表示。只要行边界或列边界交错,所有位置就已经输出,整个过程既不重复也不遗漏。

解题步骤

  1. 先判断空矩阵或空行,直接返回空数组,避免访问不存在的第一行。
  2. 将四个边界初始化为整个矩阵,准备容量为元素总数的结果容器。
  3. 当 top <= bottom 且 left <= right 时,读取上边并执行 top++,再读取右边并执行 right--。
  4. 若仍有行,反向读取下边并上移 bottom;若仍有列,向上读取左边并右移 left。
  5. 重复收缩直到边界交错,返回结果。

代码实现

class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }

        int rows = matrix.length;
        int cols = matrix[0].length;
        int[] res = new int[rows * cols];
        int idx = 0;
        int top = 0;
        int bottom = rows - 1;
        int left = 0;
        int right = cols - 1;

        while (top <= bottom && left <= right) {
            for (int col = left; col <= right; col++) {
                res[idx++] = matrix[top][col];
            }

            // 读完一边立即收缩,下一边从尚未访问的位置开始。
            top++;

            for (int row = top; row <= bottom; row++) {
                res[idx++] = matrix[row][right];
            }

            right--;

            // 最后可能只剩一行,收缩后先确认下边仍然存在。
            if (top <= bottom) {
                for (int col = right; col >= left; col--) {
                    res[idx++] = matrix[bottom][col];
                }

                bottom--;
            }

            // 最后可能只剩一列,收缩后先确认左边仍然存在。
            if (left <= right) {
                for (int row = bottom; row >= top; row--) {
                    res[idx++] = matrix[row][left];
                }

                left++;
            }
        }

        return res;
    }
}
func spiralOrder(matrix [][]int) []int {
    if len(matrix) == 0 || len(matrix[0]) == 0 {
        return []int{}
    }

    rows := len(matrix)
    cols := len(matrix[0])
    res := make([]int, 0, rows*cols)
    top := 0
    bottom := rows - 1
    left := 0
    right := cols - 1

    for top <= bottom && left <= right {
        for col := left; col <= right; col++ {
            res = append(res, matrix[top][col])
        }
        // 读完一边立即收缩,下一边从尚未访问的位置开始。
        top++
        for row := top; row <= bottom; row++ {
            res = append(res, matrix[row][right])
        }
        right--
        // 最后可能只剩一行,收缩后先确认下边仍然存在。
        if top <= bottom {
            for col := right; col >= left; col-- {
                res = append(res, matrix[bottom][col])
            }
            bottom--
        }
        // 最后可能只剩一列,收缩后先确认左边仍然存在。
        if left <= right {
            for row := bottom; row >= top; row-- {
                res = append(res, matrix[row][left])
            }
            left++
        }
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(mn)$,每个格子恰好写入结果一次。
  • 空间复杂度:$O(1)$,只使用四个边界和结果下标;若计入返回数组则为 $O(mn)$。

关键点总结

[!green]

  • 四边界是“未访问矩形”的最小充分状态,比 visited 数组更省空间。
  • 每输出一条边就立即收缩对应边界,下一条边从收缩后的端点开始,避免重复角点。
  • 下边与左边读取前必须重新判断边界,这是单行、单列正确性的关键。

易错点总结

[!yellow]

  • 未判空就访问 matrix[0]:matrix=[] 会直接越界。
  • 下边读取前不检查 top <= bottom:单行矩阵会正向、反向各输出一次。
  • 左边读取前不检查 left <= right:单列矩阵会被重复输出。
  • 右边从旧 top 开始:右上角会与上边重复;后续边同理要避开已处理角点。
  • 输出后忘记收缩边界:下一轮会重复同一圈,甚至死循环。

相似题目

题目 难度 关联与区别
59. 螺旋矩阵 II 中等 螺旋边界更新相同,本题读取矩阵,原题按顺序把数写入矩阵。
885. 螺旋矩阵 III 中等 同样按方向轮转,本题逐层收缩矩形边界,原题允许走出矩阵再进入。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/63222519
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!