54. 螺旋矩阵 (Medium)

专题归类: 矩阵 LeetCode 链接: https://leetcode.cn/problems/spiral-matrix/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你一个 mn 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。

示例 1:

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

示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

补充说明:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100
  • 注意:m 和 n 不一定相等(矩阵不一定是方阵)

题目详细分析

数据范围分析:

  • 矩阵最大 10×10 = 100 个元素,非常小,任何合法算法都不会超时。但作为经典题,O(m×n) 是标准解法。
  • 元素值范围较小,不重要。

输入输出特征:

  • 输入是 m×n 矩阵,输出是一维数组(螺旋顺序)。
  • 螺旋顺序:→ 右 (列增加) → ↓ 下 (行增加) → ← 左 (列减少) → ↑ 上 (行减少) → 循环。
  • 矩阵不一定是方阵(m 可能不等于 n),边界控制不同。

边界条件:

  • 只有一行(m=1)→ 直接按行遍历即可。
  • 只有一列(n=1)→ 直接按列遍历即可。
  • 单元素矩阵(m=1, n=1)→ 直接返回该元素。
  • 矩阵为空(但题目说 m,n >= 1,所以不会出现)。
  • 螺旋走到最后可能只剩一行或一列——需要检查边界防止重复遍历。

隐藏条件:

  • 螺旋遍历的核心是边界收缩:每走完一条边,对应的边界就向内收缩一层。
  • 每次收缩后要检查边界是否交叉,如果交叉说明所有元素已遍历完。
  • “右→下”之后,在走”左→上”之前必须检查边界是否仍然有效,否则可能重复遍历。

小白版直白理解

想象你在一栋楼的迷宫里,迷宫是矩形,你站在左上角入口。你的任务是顺时针转圈把所有房间都走一遍,并记录房间号。

就像一条贪吃蛇:先往右走到底(但不能出墙),然后往下走到底,然后往左走到底,然后往上走到底……但每次走完一条边,墙就会向内缩进一格。

简单步骤:

  1. 往右走,走完就把”上墙”往下推一格
  2. 往下走,走完就把”右墙”往左推一格
  3. 往左走,走完就把”下墙”往上推一格
  4. 往上走,走完就把”左墙”往右推一格
  5. 重复 1~4,直到四面墙挤在一起

生活中的类似场景:

  • 剥洋葱:一层一层从外往里剥
  • 削苹果:螺旋削皮
  • 打印机的喷墨路径

解题思路

思路一:边界收缩法(推荐)

核心洞察:

维护四个边界:top, bottom, left, right。按顺时针方向遍历四条边,每遍历一条边就将对应的边界收缩一格。

关键点在于:在遍历下边和左边之前,需要检查边界是否已经交叉,以防只剩一行或一列时重复遍历。

def spiralOrder(matrix):
    res = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    
    while top <= bottom and left <= right:
        # 1. 从左到右遍历上边
        for j in range(left, right + 1):
            res.append(matrix[top][j])
        top += 1
        
        # 2. 从上到下遍历右边
        for i in range(top, bottom + 1):
            res.append(matrix[i][right])
        right -= 1
        
        # 3. 从右到左遍历下边(需检查 top <= bottom)
        if top <= bottom:
            for j in range(right, left - 1, -1):
                res.append(matrix[bottom][j])
            bottom -= 1
        
        # 4. 从下到上遍历左边(需检查 left <= right)
        if left <= right:
            for i in range(bottom, top - 1, -1):
                res.append(matrix[i][left])
            left += 1
    
    return res

复杂度: O(m × n) 时间,O(1) 额外空间(输出数组不计)。

思路二:方向数组 + 访问标记

定义方向数组 [(0,1), (1,0), (0,-1), (-1,0)] 分别对应右、下、左、上。当遇到边界或已访问的元素时转向。

def spiralOrder_direction(matrix):
    m, n = len(matrix), len(matrix[0])
    # 方向:右、下、左、上
    dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    visited = [[False] * n for _ in range(m)]
    
    res = []
    row = col = dir_idx = 0
    
    for _ in range(m * n):
        res.append(matrix[row][col])
        visited[row][col] = True
        
        # 计算下一步
        nr, nc = row + dirs[dir_idx][0], col + dirs[dir_idx][1]
        
        # 如果下一步越界或已访问,转向
        if nr < 0 or nr >= m or nc < 0 or nc >= n or visited[nr][nc]:
            dir_idx = (dir_idx + 1) % 4
            nr, nc = row + dirs[dir_idx][0], col + dirs[dir_idx][1]
        
        row, col = nr, nc
    
    return res

复杂度: O(m × n) 时间,O(m × n) 空间(visited 数组)。

思路三:逐层剥离(递归思路)

递归处理矩阵的每一层(从外到内),每次处理一圈后递归处理内层子矩阵。

def spiralOrder_recursive(matrix):
    res = []
    
    def add_layer(t, b, l, r):
        if t > b or l > r:
            return
        # 上边:左 → 右
        for j in range(l, r + 1):
            res.append(matrix[t][j])
        # 右边:上 → 下
        for i in range(t + 1, b + 1):
            res.append(matrix[i][r])
        # 下边和左边需要检查
        if t < b:
            for j in range(r - 1, l - 1, -1):
                res.append(matrix[b][j])
        if l < r:
            for i in range(b - 1, t, -1):
                res.append(matrix[i][l])
        # 递归处理内层
        add_layer(t + 1, b - 1, l + 1, r - 1)
    
    add_layer(0, len(matrix) - 1, 0, len(matrix[0]) - 1)
    return res

易错点

  • 下边和左边的边界检查:遍历完上边和右边后,topright 已更新。在遍历下边前必须检查 top <= bottom(防止只剩一列时重复遍历),在遍历左边前必须检查 left <= right(防止只剩一行时重复遍历)。
  • range 的步长:从右到左遍历时 range(right, left-1, -1),从下到上遍历时 range(bottom, top-1, -1)。注意 left-1top-1 是因为 range 是左闭右开区间。
  • 非方阵处理:当 m ≠ n 时,螺旋遍历到最后可能只剩一行或一列,边界检查变得尤为重要。
  • 单行/单列:如果只有一行(top == bottom),遍历上边就够了,不需要再遍历下边。
  • 死循环:没有正确更新边界或边界检查条件不对可能导致死循环。

框架提炼

螺旋遍历通用模板(边界收缩法):

def spiralOrder(matrix):
    if not matrix or not matrix[0]:
        return []
    
    # Step 1: 初始化四边界
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    result = []
    
    # Step 2: 循环遍历
    while top <= bottom and left <= right:
        # 上边 →
        for j in range(left, right + 1):
            result.append(matrix[top][j])
        top += 1
        
        # 右边 ↓
        for i in range(top, bottom + 1):
            result.append(matrix[i][right])
        right -= 1
        
        # 下边 ← (需 top <= bottom)
        if top <= bottom:
            for j in range(right, left - 1, -1):
                result.append(matrix[bottom][j])
            bottom -= 1
        
        # 左边 ↑ (需 left <= right)
        if left <= right:
            for i in range(bottom, top - 1, -1):
                result.append(matrix[i][left])
            left += 1
    
    return result

变体应用:

  • 螺旋矩阵 II(生成螺旋矩阵)→ 59-螺旋矩阵 II
  • 螺旋遍历的逆过程
  • 从任意起点、任意方向开始螺旋遍历

关联题目

  • 59-螺旋矩阵 II — 本题的逆向问题:给定 n,生成一个 n×n 的螺旋矩阵。同样用边界收缩法,但方向是赋值而不是读取。
  • 48-旋转图像 — 矩阵旋转问题,同样是矩阵操作,但用的是转置+翻转的组合思路。
  • 73-矩阵置零 — 也是矩阵原地操作问题,核心是标记与修改分离。
  • 885-螺旋矩阵 III — 变体:从矩阵中心开始螺旋遍历,边界控制更复杂。