目描述
给你一个 m 行 n 列的矩阵 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
解题思路
螺旋矩阵是一类经典的矩阵遍历问题,核心是模拟顺时针遍历过程 。我们需要按照"向右→向下→向左→向上"的顺序遍历矩阵,同时处理好边界条件,避免重复访问-3-7。
下面介绍三种解法,从直观到最优。
解法一:按层模拟(边界收缩法)【最直观】
算法思想
将矩阵看作由若干层组成,每一层是一个矩形环。我们按照顺时针方向遍历每一层,然后收缩边界,进入内层继续遍历,直到所有元素都被访问-6-10。
定义四个边界:
-
top:上边界,初始为0 -
bottom:下边界,初始为m-1 -
left:左边界,初始为0 -
right:右边界,初始为n-1
每遍历完一层,就收缩边界:
-
遍历完上边,
top++ -
遍历完右边,
right-- -
遍历完下边,
bottom-- -
遍历完左边,
left++
代码实现
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 1. 从左到右遍历上边
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++; // 上边界收缩
// 2. 从上到下遍历右边
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--; // 右边界收缩
// 3. 从右到左遍历下边(需要检查上下边界是否交错)
if (top <= bottom) {
for (int i = right; i >= left; i--) {
result.add(matrix[bottom][i]);
}
bottom--; // 下边界收缩
}
// 4. 从下到上遍历左边(需要检查左右边界是否交错)
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++; // 左边界收缩
}
}
return result;
}
}
图解示例
以 matrix = [[1,2,3],[4,5,6],[7,8,9]] 为例:
| 步骤 | 操作 | 边界状态 | 遍历结果 |
|---|---|---|---|
| 初始 | - | top=0, bottom=2, left=0, right=2 | [] |
| 第1圈-上边 | 从左到右遍历第0行 | top=1, bottom=2, left=0, right=2 | [1,2,3] |
| 第1圈-右边 | 从上到下遍历第2列 | top=1, bottom=2, left=0, right=1 | [1,2,3,6,9] |
| 第1圈-下边 | 从右到左遍历第2行 | top=1, bottom=1, left=0, right=1 | [1,2,3,6,9,8,7] |
| 第1圈-左边 | 从下到上遍历第0列 | top=2, bottom=1, left=1, right=1 | [1,2,3,6,9,8,7,4] |
| 第2圈 | top>bottom,结束 | - | 最终结果:[1,2,3,6,9,8,7,4,5] |
复杂度分析
-
时间复杂度: O(m×n),每个元素恰好访问一次
-
空间复杂度: O(1),只使用了常数个边界变量
解法二:方向模拟法(偏移量数组)【推荐】
算法思想
使用方向数组来控制遍历方向,同时用一个访问标记数组记录哪些位置已经被访问过-5-9。
定义四个方向:
-
向右:
(0, 1) -
向下:
(1, 0) -
向左:
(0, -1) -
向上:
(-1, 0)
从 (0,0) 开始,按照当前方向移动,如果下一个位置越界或已经访问过,就顺时针旋转90度(改变方向),然后继续移动-2。
代码实现
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int m = matrix.length; // 行数
int n = matrix[0].length; // 列数
boolean[][] visited = new boolean[m][n]; // 标记是否访问过
// 方向数组:右、下、左、上
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int directionIndex = 0; // 当前方向索引
int row = 0, col = 0; // 当前位置
for (int i = 0; i < m * n; i++) {
result.add(matrix[row][col]);
visited[row][col] = true;
// 计算下一个位置
int nextRow = row + directions[directionIndex][0];
int nextCol = col + directions[directionIndex][1];
// 如果下一个位置越界或已访问,需要转向
if (nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n
|| visited[nextRow][nextCol]) {
directionIndex = (directionIndex + 1) % 4; // 顺时针旋转方向
nextRow = row + directions[directionIndex][0];
nextCol = col + directions[directionIndex][1];
}
row = nextRow;
col = nextCol;
}
return result;
}
}
图解示例
以 matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 为例:
| 步数 | 当前位置 | 当前方向 | 操作 | 结果集 |
|---|---|---|---|---|
| 1 | (0,0) | 右 | 添加1 | [1] |
| 2 | (0,1) | 右 | 添加2 | [1,2] |
| 3 | (0,2) | 右 | 添加3 | [1,2,3] |
| 4 | (0,3) | 右 | 添加4,下一个(0,4)越界,转向下 | [1,2,3,4] |
| 5 | (1,3) | 下 | 添加8 | [1,2,3,4,8] |
| 6 | (2,3) | 下 | 添加12,下一个(3,3)越界,转向左 | [1,2,3,4,8,12] |
| 7 | (2,2) | 左 | 添加11 | [1,2,3,4,8,12,11] |
| 8 | (2,1) | 左 | 添加10 | [1,2,3,4,8,12,11,10] |
| 9 | (2,0) | 左 | 添加9,下一个(2,-1)越界,转向上 | [1,2,3,4,8,12,11,10,9] |
| 10 | (1,0) | 上 | 添加5 | [1,2,3,4,8,12,11,10,9,5] |
| 11 | (1,1) | 上 | 添加6,下一个(0,1)已访问,转向右 | [1,2,3,4,8,12,11,10,9,5,6] |
| 12 | (1,2) | 右 | 添加7 | [1,2,3,4,8,12,11,10,9,5,6,7] |
复杂度分析
-
时间复杂度: O(m×n),每个元素恰好访问一次
-
空间复杂度: O(m×n),需要 visited 数组记录访问状态
解法三:逐层遍历(代码随想录版)
算法思想
这种方法与解法一类似,但采用了更规范的"左闭右开"区间处理方式。通过计算循环圈数,逐层处理-1-7。
代码实现
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
int m = matrix.length; // 行数
int n = matrix[0].length; // 列数
int start = 0; // 每一圈的起始位置
int loop = Math.min(m, n) / 2; // 循环的圈数
int mid = loop; // 中间位置
int offset = 1; // 控制每一条边遍历的长度
int i, j;
while (loop-- > 0) {
i = start;
j = start;
// 从左到右遍历上边
for (; j < n - offset; j++) {
result.add(matrix[i][j]);
}
// 从上到下遍历右边
for (; i < m - offset; i++) {
result.add(matrix[i][j]);
}
// 从右到左遍历下边
for (; j > start; j--) {
result.add(matrix[i][j]);
}
// 从下到上遍历左边
for (; i > start; i--) {
result.add(matrix[i][j]);
}
start++;
offset++;
}
// 如果行和列的最小值是奇数,需要处理中间行或中间列
if (Math.min(m, n) % 2 != 0) {
if (m > n) { // 行数大于列数,剩下中间列
for (int k = mid; k < mid + m - n + 1; k++) {
result.add(matrix[k][mid]);
}
} else { // 列数大于等于行数,剩下中间行
for (int k = mid; k < mid + n - m + 1; k++) {
result.add(matrix[mid][k]);
}
}
}
return result;
}
}
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 边界收缩法 | O(m×n) | O(1) | 思路直观,无需额外空间 | 需要处理边界交错情况 |
| 方向模拟法 | O(m×n) | O(m×n) | 逻辑清晰,易于理解 | 需要visited数组,空间较大 |
| 逐层遍历法 | O(m×n) | O(1) | 代码规范,左闭右开原则 | 实现稍复杂,需处理中间行列 |
代码要点详解
1. 边界收缩法中的条件判断
java
// 遍历下边前需要判断top <= bottom
if (top <= bottom) {
for (int i = right; i >= left; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
}
// 遍历左边前需要判断left <= right
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++;
}
这两个判断很重要,防止在只剩一行或一列时重复遍历-6。
2. 方向模拟法中的转向逻辑
java
// 计算下一个位置
int nextRow = row + directions[directionIndex][0];
int nextCol = col + directions[directionIndex][1];
// 如果下一个位置越界或已访问,需要转向
if (nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n
|| visited[nextRow][nextCol]) {
directionIndex = (directionIndex + 1) % 4; // 顺时针旋转方向
nextRow = row + directions[directionIndex][0];
nextCol = col + directions[directionIndex][1];
}
方向数组 {0,1} 表示向右,{1,0} 表示向下,{0,-1} 表示向左,{-1,0} 表示向上。(directionIndex + 1) % 4 实现了顺时针转向-2-5。
常见错误与注意事项
-
忘记处理空矩阵 :输入可能为
null或长度为0,需要提前返回空列表 -
边界条件错误:在边界收缩法中,遍历下边和左边前必须检查边界是否有效
-
重复访问:方向模拟法中必须用 visited 数组记录访问状态
-
索引越界:计算下一个位置时,必须先检查是否越界
-
单行或单列的特殊处理:当矩阵只有一行或一列时,要避免重复遍历
面试建议
-
优先写出边界收缩法:这是最直观的解法,代码简洁,无需额外空间
-
可以提及方向模拟法:作为对比,展示你知道用方向数组模拟遍历的思路
-
注意边界条件处理:面试官通常会关注你对边界情况的考虑
-
画图演示:面试时可以用简单例子画图,帮助解释算法过程
相关题目推荐
-
力扣 59. 螺旋矩阵 II(逆过程:生成螺旋矩阵)
-
力扣 54. 螺旋矩阵(本题)
-
力扣 885. 螺旋矩阵 III
-
力扣 2326. 螺旋矩阵 IV
-
剑指 Offer 29. 顺时针打印矩阵(与本题完全相同)
以上就是力扣54题"螺旋矩阵"的Java解法详细解析,重点掌握边界收缩法 和方向模拟法这两种经典解法。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!