LeetCode 54. 螺旋矩阵 - 完整解法详解
一、问题理解
问题描述
给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例
python
输入: matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出: [1,2,3,6,9,8,7,4,5]
解释: 顺时针螺旋遍历
输入: 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]
解释: 顺时针螺旋遍历
要求
-
按顺时针螺旋顺序遍历矩阵
-
返回包含所有元素的列表
-
时间复杂度:O(m × n)
-
空间复杂度:O(1)(不考虑输出列表)
二、核心思路:模拟螺旋路径
基本思想
我们可以模拟一个人按照顺时针方向在矩阵中行走,每次走到边界或者已经访问过的位置就右转90度,继续前进,直到访问完所有元素。
关键点
-
方向控制:使用方向数组控制前进方向(右、下、左、上)
-
边界判断:判断是否越界或者已经访问过
-
转向机制:遇到边界或已访问位置时右转
三、代码逐行解析
方法一:方向数组模拟法(简洁高效)
Python 解法
python
from typing import List
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
# 定义四个方向:右(0,1)、下(1,0)、左(0,-1)、上(-1,0)
DIRS = (0, 1), (1, 0), (0, -1), (-1, 0)
m, n = len(matrix), len(matrix[0]) # 获取矩阵的行数和列数
ans = [] # 存储结果的列表
i = j = di = 0 # 初始化当前位置(i,j)和当前方向索引di
# 一共需要访问 m*n 个元素
for _ in range(m * n):
# 将当前位置的元素加入结果列表
ans.append(matrix[i][j])
# 将访问过的位置标记为None,表示已访问
matrix[i][j] = None
# 计算下一步的位置
x, y = i + DIRS[di][0], j + DIRS[di][1]
# 如果下一步超出边界或者已经访问过
if x < 0 or x >= m or y < 0 or y >= n or matrix[x][y] is None:
di = (di + 1) % 4 # 右转90度(改变方向)
# 按照新的方向前进一步
i += DIRS[di][0]
j += DIRS[di][1]
return ans
Java 解法
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
// 定义四个方向:右、下、左、上
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int m = matrix.length, n = matrix[0].length;
List<Integer> ans = new ArrayList<>();
int i = 0, j = 0, di = 0;
for (int k = 0; k < m * n; k++) {
ans.add(matrix[i][j]);
matrix[i][j] = Integer.MIN_VALUE; // 标记已访问
// 计算下一步位置
int x = i + dirs[di][0];
int y = j + dirs[di][1];
// 如果越界或已访问,改变方向
if (x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] == Integer.MIN_VALUE) {
di = (di + 1) % 4;
}
// 前进一步
i += dirs[di][0];
j += dirs[di][1];
}
return ans;
}
}
方法二:边界收缩法(不使用标记)
Python 解法
python
from typing import List
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
ans = []
# 定义四个边界:上、下、左、右
top, bottom = 0, m - 1
left, right = 0, n - 1
while top <= bottom and left <= right:
# 1. 从左到右遍历上边界
for j in range(left, right + 1):
ans.append(matrix[top][j])
top += 1 # 上边界下移
# 2. 从上到下遍历右边界
for i in range(top, bottom + 1):
ans.append(matrix[i][right])
right -= 1 # 右边界左移
# 3. 从右到左遍历下边界(确保还有行)
if top <= bottom:
for j in range(right, left - 1, -1):
ans.append(matrix[bottom][j])
bottom -= 1 # 下边界上移
# 4. 从下到上遍历左边界(确保还有列)
if left <= right:
for i in range(bottom, top - 1, -1):
ans.append(matrix[i][left])
left += 1 # 左边界右移
return ans
Java 解法
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
List<Integer> ans = new ArrayList<>();
int top = 0, bottom = m - 1;
int left = 0, right = n - 1;
while (top <= bottom && left <= right) {
// 从左到右
for (int j = left; j <= right; j++) {
ans.add(matrix[top][j]);
}
top++;
// 从上到下
for (int i = top; i <= bottom; i++) {
ans.add(matrix[i][right]);
}
right--;
// 从右到左
if (top <= bottom) {
for (int j = right; j >= left; j--) {
ans.add(matrix[bottom][j]);
}
bottom--;
}
// 从下到上
if (left <= right) {
for (int i = bottom; i >= top; i--) {
ans.add(matrix[i][left]);
}
left++;
}
}
return ans;
}
}
四、Java 与 Python 语法对比
1. 方向数组定义
| 操作 | Java | Python |
|---|---|---|
| 定义二维数组 | int[][] dirs = {``{0,1},{1,0},{0,-1},{-1,0}}; |
DIRS = (0,1),(1,0),(0,-1),(-1,0) |
| 访问元素 | dirs[di][0] |
DIRS[di][0] |
2. 列表/数组操作
| 操作 | Java | Python |
|---|---|---|
| 创建列表 | List<Integer> ans = new ArrayList<>(); |
ans = [] |
| 添加元素 | ans.add(value); |
ans.append(value) |
| 获取长度 | ans.size() |
len(ans) |
3. 循环控制
| 操作 | Java | Python |
|---|---|---|
| 固定次数循环 | for(int k=0; k<m*n; k++) |
for _ in range(m*n): |
| 范围循环 | for(int j=left; j<=right; j++) |
for j in range(left, right+1): |
| 反向循环 | for(int j=right; j>=left; j--) |
for j in range(right, left-1, -1): |
4. 特殊值标记
| 操作 | Java | Python |
|---|---|---|
| 标记已访问 | matrix[i][j] = Integer.MIN_VALUE; |
matrix[i][j] = None |
| 检查标记 | matrix[x][y] == Integer.MIN_VALUE |
matrix[x][y] is None |
五、实例演示
示例1:matrix = [[1,2,3],[4,5,6],[7,8,9]]
方法一(方向数组模拟法)步骤:
-
初始化:
-
m=3, n=3 -
i=0, j=0, di=0(方向:右) -
ans = []
-
-
遍历过程:
-
第1步:
(0,0)=1,标记为None,计算下一步(0,1)正常,前进到(0,1) -
第2步:
(0,1)=2,标记为None,计算下一步(0,2)正常,前进到(0,2) -
第3步:
(0,2)=3,标记为None,计算下一步(0,3)越界,转向为下(1),前进到(1,2) -
第4步:
(1,2)=6,标记为None,计算下一步(2,2)正常,前进到(2,2) -
第5步:
(2,2)=9,标记为None,计算下一步(3,2)越界,转向为左(2),前进到(2,1) -
第6步:
(2,1)=8,标记为None,计算下一步(2,0)正常,前进到(2,0) -
第7步:
(2,0)=7,标记为None,计算下一步(1,0)正常,前进到(1,0) -
第8步:
(1,0)=4,标记为None,计算下一步(1,1)正常,前进到(1,1) -
第9步:
(1,1)=5,标记为None,结束
-
-
最终结果 :
[1,2,3,6,9,8,7,4,5]
方法二(边界收缩法)步骤:
-
初始边界:
top=0, bottom=2, left=0, right=2
-
第一轮循环:
-
从左到右:
(0,0),(0,1),(0,2)→[1,2,3] -
top=1 -
从上到下:
(1,2),(2,2)→[1,2,3,6,9] -
right=1 -
从右到左:
(2,1),(2,0)→[1,2,3,6,9,8,7] -
bottom=1 -
从下到上:
(1,0),(1,1)→[1,2,3,6,9,8,7,4,5] -
left=1
-
-
循环结束 (
top=2 > bottom=1) -
最终结果 :
[1,2,3,6,9,8,7,4,5]
示例2:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
方法一步骤(简要):
-
第1-4步:向右遍历第一行
[1,2,3,4] -
遇到边界转向下,遍历最后一列
[8,12] -
遇到边界转向左,遍历最后一行
[11,10,9] -
遇到边界转向上,遍历第一列剩余
[5] -
继续向右,遍历中间行
[6,7] -
最终结果:
[1,2,3,4,8,12,11,10,9,5,6,7]
六、关键细节解析
1. 为什么使用None作为标记?
-
Python中None是一个特殊的单例对象,适合作为标记值
-
确保不会与矩阵中的任何有效值冲突(矩阵中可能包含0或其他值)
-
使用
is None检查比== None更高效
2. 方向数组为什么按这个顺序?
-
(0,1):向右(列增加) -
(1,0):向下(行增加) -
(0,-1):向左(列减少) -
(-1,0):向上(行减少) -
这个顺序正好是顺时针螺旋的顺序
3. 如何确保不会无限循环?
-
循环次数固定为
m*n,每个元素只访问一次 -
每次访问后标记该位置,避免重复访问
-
当所有元素都被访问后,循环自然结束
4. 边界收缩法中的if条件为什么必要?
python
if top <= bottom: # 确保还有行可以遍历
if left <= right: # 确保还有列可以遍历
-
在矩形矩阵中,最后可能只剩一行或一列
-
如果没有这些条件,会重复遍历已经遍历过的元素
-
这些条件确保只在有多行/多列时才进行对应方向的遍历
5. 如何处理1×n或m×1的矩阵?
-
方向数组法:天然支持,因为标记机制确保不会走回头路
-
边界收缩法:需要if条件来避免重复遍历
-
两种方法都能正确处理特殊形状的矩阵
七、复杂度分析
方法一(方向数组模拟法)
-
时间复杂度:O(m × n),每个元素访问一次
-
空间复杂度:O(1),如果不考虑输出列表和修改原矩阵。如果考虑原矩阵被修改,则是原地算法。或者可以使用额外O(m×n)空间来复制矩阵以避免修改原矩阵。
方法二(边界收缩法)
-
时间复杂度:O(m × n),每个元素访问一次
-
空间复杂度:O(1),只使用了几个变量,不修改原矩阵
八、其他解法
解法一:使用visited矩阵(更直观但空间复杂度高)
python
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
visited = [[False] * n for _ in range(m)]
ans = []
# 四个方向:右、下、左、上
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
i = j = di = 0
for _ in range(m * n):
ans.append(matrix[i][j])
visited[i][j] = True
# 计算下一步
x, y = i + dirs[di][0], j + dirs[di][1]
# 如果越界或已访问,改变方向
if x < 0 or x >= m or y < 0 or y >= n or visited[x][y]:
di = (di + 1) % 4
i += dirs[di][0]
j += dirs[di][1]
return ans
解法二:递归解法
python
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
def spiral_layer(top, bottom, left, right):
# 递归终止条件
if top > bottom or left > right:
return []
result = []
# 遍历上边界
for j in range(left, right + 1):
result.append(matrix[top][j])
# 遍历右边界
for i in range(top + 1, bottom + 1):
result.append(matrix[i][right])
# 如果有多行,遍历下边界
if top < bottom:
for j in range(right - 1, left - 1, -1):
result.append(matrix[bottom][j])
# 如果有多列,遍历左边界
if left < right:
for i in range(bottom - 1, top, -1):
result.append(matrix[i][left])
# 递归处理内层
return result + spiral_layer(top + 1, bottom - 1, left + 1, right - 1)
m, n = len(matrix), len(matrix[0])
return spiral_layer(0, m - 1, 0, n - 1)
九、常见问题与解答
Q1: 如果矩阵为空怎么办?
A1: 题目保证矩阵至少包含一个元素。但实际编程中可以添加边界检查:
python
if not matrix or not matrix[0]:
return []
Q2: 如何逆时针螺旋遍历?
A2: 只需改变方向数组的顺序:
python
# 逆时针:下、右、上、左 或 上、左、下、右
DIRS = (1, 0), (0, 1), (-1, 0), (0, -1) # 下、右、上、左
Q3: 如果不想修改原矩阵怎么办?
A3: 有两种方法:
-
复制一份矩阵进行修改
-
使用visited矩阵记录访问状态
-
使用边界收缩法(不修改原矩阵)
Q4: 这个方法适用于非矩形矩阵吗?
A4: 题目给定的都是矩形矩阵(每行长度相同)。对于不规则矩阵,需要特殊处理。
Q5: 如何从外向内螺旋遍历?
A5: 我们现在的解法就是从外向内螺旋遍历。如果要指定起始点,可以调整初始位置和边界。
十、相关题目
1. LeetCode 59. 螺旋矩阵 II
python
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
# 创建 n×n 矩阵
matrix = [[0] * n for _ in range(n)]
# 定义方向:右、下、左、上
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
i = j = di = 0
for num in range(1, n * n + 1):
matrix[i][j] = num
# 计算下一步
x, y = i + dirs[di][0], j + dirs[di][1]
# 如果越界或已填充,改变方向
if x < 0 or x >= n or y < 0 or y >= n or matrix[x][y] != 0:
di = (di + 1) % 4
i += dirs[di][0]
j += dirs[di][1]
return matrix
2. LeetCode 885. 螺旋矩阵 III
python
class Solution:
def spiralMatrixIII(self, rows: int, cols: int, rStart: int, cStart: int) -> List[List[int]]:
# 方向:右、下、左、上
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
result = []
step = 0 # 当前方向的步长
dir_index = 0 # 当前方向
r, c = rStart, cStart
# 当结果中的坐标数小于总格子数时继续
while len(result) < rows * cols:
# 右和下方向时步长增加,左和上方向时步长不变
if dir_index == 0 or dir_index == 2:
step += 1
# 沿当前方向走step步
for _ in range(step):
# 如果当前位置在网格内,加入结果
if 0 <= r < rows and 0 <= c < cols:
result.append([r, c])
# 移动
r += dirs[dir_index][0]
c += dirs[dir_index][1]
# 改变方向
dir_index = (dir_index + 1) % 4
return result
十一、总结
核心要点
-
方向控制:使用方向数组简化方向切换逻辑
-
边界处理:通过标记或边界收缩避免重复访问
-
循环控制:固定循环次数确保访问所有元素
算法步骤(方向数组法)
-
定义四个方向:右、下、左、上
-
初始化当前位置和方向
-
循环 m×n 次:
-
将当前位置元素加入结果
-
标记当前位置为已访问
-
计算下一步位置
-
如果下一步越界或已访问,改变方向
-
按照新方向前进一步
-
-
返回结果列表
时间复杂度与空间复杂度
-
时间复杂度:O(m × n),每个元素访问一次
-
空间复杂度:
-
方向数组法:O(1)(如果允许修改原矩阵)
-
边界收缩法:O(1)(不修改原矩阵)
-
输出列表:O(m × n)(不计入空间复杂度)
-
适用场景
-
需要按特定顺序遍历矩阵
-
矩阵遍历问题
-
需要控制遍历路径的问题
扩展思考
螺旋矩阵的解法展示了如何通过方向数组和状态标记来控制复杂遍历路径,这种思想可以应用于:
-
其他类型的矩阵遍历(蛇形、之字形等)
-
游戏中的角色移动控制
-
机器人路径规划
-
图像处理中的像素遍历
掌握螺旋矩阵的解法不仅能够解决这类特定问题,还能提高对矩阵操作和状态控制的理解,是面试中常见的算法题目。