给你一个 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]
提示:
这道题要求我们按照螺旋顺序遍历一个二维矩阵,并将遍历结果作为一维数组返回。具体来说:
螺旋顺序的遍历路径为:
关键点:
最直观的解法是模拟螺旋遍历的过程,使用四个变量表示当前的边界:
top
、right
、bottom
、left
表示四个边界这种方法直观模拟了题目要求的螺旋遍历过程。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
// 定义四个边界
int top = 0;
int right = matrix[0].length - 1;
int bottom = matrix.length - 1;
int left = 0;
// 循环遍历,直到所有元素都被访问
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++; // 上边界下移
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--; // 右边界左移
// 检查是否还有行未被遍历
if (top <= bottom) {
// 从右到左遍历下边界
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--; // 下边界上移
}
// 检查是否还有列未被遍历
if (left <= right) {
// 从下到上遍历左边界
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++; // 左边界右移
}
}
return result;
}
}
详细解释每一步的意义和实现:
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
// 定义四个边界
int top = 0;
int right = matrix[0].length - 1;
int bottom = matrix.length - 1;
int left = 0;
top
表示上边界的行索引,初始为0right
表示右边界的列索引,初始为矩阵的列数减1bottom
表示下边界的行索引,初始为矩阵的行数减1left
表示左边界的列索引,初始为0// 循环遍历,直到所有元素都被访问
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++; // 上边界下移
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--; // 右边界左移
// 检查是否还有行未被遍历
if (top <= bottom) {
// 从右到左遍历下边界
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--; // 下边界上移
}
// 检查是否还有列未被遍历
if (left <= right) {
// 从下到上遍历左边界
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++; // 左边界右移
}
}
对于每一条边的遍历:
top
行,从left
列遍历到right
列right
列,从top
行遍历到bottom
行bottom
行,从right
列遍历到left
列left
列,从bottom
行遍历到top
行在遍历每条边后,对应的边界会向内收缩:
top++
right--
bottom--
left++
特别注意的是,在遍历下边界和左边界之前,需要检查边界条件:
if (top <= bottom)
,确保还有行未被遍历if (left <= right)
,确保还有列未被遍历这是为了处理矩阵行数或列数为奇数的情况,避免重复遍历。
四边界模拟法是解决螺旋矩阵问题的基础方法,适用于大多数情况。它直观、易于理解和实现,是面试中常用的解法。
另一种常用的解法是使用方向数组来模拟螺旋遍历的过程:
这种方法通过不断改变方向来实现螺旋遍历,更加灵活。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
int m = matrix.length;
int n = matrix[0].length;
// 创建访问标记数组,初始值为false,表示未访问
boolean[][] visited = new boolean[m][n];
// 定义四个方向:向右、向下、向左、向上
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int dirIndex = 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[dirIndex][0];
int nextCol = col + directions[dirIndex][1];
// 如果下一个位置超出边界或已访问,则改变方向
if (nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n || visited[nextRow][nextCol]) {
dirIndex = (dirIndex + 1) % 4; // 顺时针旋转,切换到下一个方向
nextRow = row + directions[dirIndex][0];
nextCol = col + directions[dirIndex][1];
}
// 更新当前位置
row = nextRow;
col = nextCol;
}
return result;
}
}
详细解释每一步的意义和实现:
// 创建访问标记数组,初始值为false,表示未访问
boolean[][] visited = new boolean[m][n];
false
,表示所有元素都未被访问// 定义四个方向:向右、向下、向左、向上
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int dirIndex = 0; // 初始方向为向右
directions
,表示四个方向的行列变化{0, 1}
表示向右移动(行不变,列加1){1, 0}
表示向下移动(行加1,列不变){0, -1}
表示向左移动(行不变,列减1){-1, 0}
表示向上移动(行减1,列不变)dirIndex
变量记录当前的方向,初始值为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[dirIndex][0];
int nextCol = col + directions[dirIndex][1];
// 如果下一个位置超出边界或已访问,则改变方向
if (nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n || visited[nextRow][nextCol]) {
dirIndex = (dirIndex + 1) % 4; // 顺时针旋转,切换到下一个方向
nextRow = row + directions[dirIndex][0];
nextCol = col + directions[dirIndex][1];
}
// 更新当前位置
row = nextRow;
col = nextCol;
}
(dirIndex + 1) % 4
实现方向的循环变化:0->1->2->3->0->…两种解法的核心思想不同:
各有优缺点:
第三种解法是按照层次来遍历矩阵,将矩阵看作是由多个"层"组成的,从外到内一层一层地遍历:
topRow
、右边界rightCol
、下边界bottomRow
、左边界leftCol
这种方法的优点是思路清晰,易于理解,且无需使用额外的空间来记录已访问的元素。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
int m = matrix.length;
int n = matrix[0].length;
// 定义层次的边界
int topRow = 0;
int rightCol = n - 1;
int bottomRow = m - 1;
int leftCol = 0;
// 层次遍历
while (topRow <= bottomRow && leftCol <= rightCol) {
// 遍历当前层的上边
for (int j = leftCol; j <= rightCol; j++) {
result.add(matrix[topRow][j]);
}
// 遍历当前层的右边
for (int i = topRow + 1; i <= bottomRow; i++) {
result.add(matrix[i][rightCol]);
}
// 如果当前层不止一行,则遍历下边
if (topRow < bottomRow) {
for (int j = rightCol - 1; j >= leftCol; j--) {
result.add(matrix[bottomRow][j]);
}
}
// 如果当前层不止一列,则遍历左边
if (leftCol < rightCol) {
for (int i = bottomRow - 1; i > topRow; i--) {
result.add(matrix[i][leftCol]);
}
}
// 向内收缩,进入下一层
topRow++;
rightCol--;
bottomRow--;
leftCol++;
}
return result;
}
}
详细解释每一步的意义和实现:
// 定义层次的边界
int topRow = 0;
int rightCol = n - 1;
int bottomRow = m - 1;
int leftCol = 0;
topRow
表示上边的行索引rightCol
表示右边的列索引bottomRow
表示下边的行索引leftCol
表示左边的列索引// 层次遍历
while (topRow <= bottomRow && leftCol <= rightCol) {
// 遍历当前层的四条边
// ...
// 向内收缩,进入下一层
topRow++;
rightCol--;
bottomRow--;
leftCol++;
}
// 遍历当前层的上边
for (int j = leftCol; j <= rightCol; j++) {
result.add(matrix[topRow][j]);
}
topRow
,列索引从leftCol
到rightCol
// 遍历当前层的右边
for (int i = topRow + 1; i <= bottomRow; i++) {
result.add(matrix[i][rightCol]);
}
rightCol
,行索引从topRow+1
到bottomRow
topRow+1
开始是为了避免重复遍历右上角的元素// 如果当前层不止一行,则遍历下边
if (topRow < bottomRow) {
for (int j = rightCol - 1; j >= leftCol; j--) {
result.add(matrix[bottomRow][j]);
}
}
topRow < bottomRow
,即当前层至少有两行时,才需要遍历下边bottomRow
,列索引从rightCol-1
到leftCol
rightCol-1
开始是为了避免重复遍历右下角的元素// 如果当前层不止一列,则遍历左边
if (leftCol < rightCol) {
for (int i = bottomRow - 1; i > topRow; i--) {
result.add(matrix[i][leftCol]);
}
}
leftCol < rightCol
,即当前层至少有两列时,才需要遍历左边leftCol
,行索引从bottomRow-1
到topRow+1
bottomRow-1
开始是为了避免重复遍历左下角的元素topRow+1
为止,而不是到topRow
,是为了避免重复遍历左上角的元素层次遍历法与四边界模拟法(解法一)很相似,主要区别在于:
层次遍历法的优点:
总体来说,层次遍历法是一种结合了四边界模拟法的直观性和方向数组法的简洁性的方法,是解决螺旋矩阵问题的优秀解法。
让我们通过几个具体的例子,详细跟踪每种解法的执行过程,以加深理解。
以示例1中的3×3矩阵为例:
[
[1,2,3],
[4,5,6],
[7,8,9]
]
使用解法一(四边界模拟法)跟踪:
初始化:
第一轮循环:
遍历上边界:添加matrix[0][0], matrix[0][1], matrix[0][2],即1, 2, 3
更新top = 1
结果:result = [1, 2, 3]
遍历右边界:添加matrix[1][2], matrix[2][2],即6, 9
更新right = 1
结果:result = [1, 2, 3, 6, 9]
遍历下边界:添加matrix[2][1], matrix[2][0],即8, 7
更新bottom = 1
结果:result = [1, 2, 3, 6, 9, 8, 7]
遍历左边界:添加matrix[1][0],即4
更新left = 1
结果:result = [1, 2, 3, 6, 9, 8, 7, 4]
第二轮循环:
遍历上边界:添加matrix[1][1],即5
更新top = 2
结果:result = [1, 2, 3, 6, 9, 8, 7, 4, 5]
此时top > bottom,循环结束
最终结果:[1, 2, 3, 6, 9, 8, 7, 4, 5]
使用解法二(方向数组模拟法)跟踪:
初始化:
遍历过程:
当前位置:(0, 0),添加1,标记为已访问
下一个位置:(0, 1),未访问,更新当前位置
结果:result = [1]
当前位置:(0, 1),添加2,标记为已访问
下一个位置:(0, 2),未访问,更新当前位置
结果:result = [1, 2]
当前位置:(0, 2),添加3,标记为已访问
下一个位置:(0, 3),超出边界,改变方向为向下
新的下一个位置:(1, 2),未访问,更新当前位置
结果:result = [1, 2, 3]
继续遍历…
最终结果:[1, 2, 3, 6, 9, 8, 7, 4, 5]
以示例2中的3×4矩阵为例:
[
[1,2,3,4],
[5,6,7,8],
[9,10,11,12]
]
使用解法三(层次遍历法)跟踪:
初始化:
第一轮循环:
第二轮循环:
循环终止:topRow > bottomRow,循环结束
最终结果:[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
考虑一个单行矩阵:
[
[1,2,3]
]
使用解法一(四边界模拟法)跟踪:
初始化:
第一轮循环:
遍历上边界:添加matrix[0][0], matrix[0][1], matrix[0][2],即1, 2, 3
更新top = 1
结果:result = [1, 2, 3]
此时top > bottom,循环结束
最终结果:[1, 2, 3]
考虑一个单列矩阵:
[
[1],
[2],
[3]
]
使用解法一(四边界模拟法)跟踪:
初始化:
第一轮循环:
遍历上边界:添加matrix[0][0],即1
更新top = 1
结果:result = [1]
遍历右边界:添加matrix[1][0], matrix[2][0],即2, 3
更新right = -1
结果:result = [1, 2, 3]
此时left > right,循环结束
最终结果:[1, 2, 3]
以下是螺旋矩阵遍历过程的动态示意:
3×3矩阵示例:
→ → →
↑ ↓
↑ ← ← ↓
3×4矩阵示例:
→ → → →
↑ ↓
↑ ← ← ← ↓
边界条件处理不当:
最常见的错误是在处理矩阵的边界时出错,特别是在处理单行或单列矩阵时。
// 错误写法:没有检查边界条件
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
// 正确写法:先检查是否还有行需要遍历
if (top <= bottom) {
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
}
遍历顺序错误:
错误地定义螺旋遍历的顺序,导致结果不符合预期。
// 错误写法:顺序混乱
// 从左到右遍历上边界
// 从右到左遍历下边界
// 从上到下遍历右边界
// 从下到上遍历左边界
// 正确写法:明确的顺时针顺序
// 从左到右遍历上边界
// 从上到下遍历右边界
// 从右到左遍历下边界
// 从下到上遍历左边界
更新边界顺序错误:
在使用四边界模拟法时,如果边界更新顺序错误,可能导致重复遍历或遗漏元素。
// 错误写法:更新边界的顺序错误
top++;
left++;
bottom--;
right--;
// 正确写法:先处理横向边界,再处理纵向边界
top++;
bottom--;
left++;
right--;
未正确处理特殊情况:
未正确处理空矩阵、单行矩阵或单列矩阵等特殊情况。
// 错误写法:没有处理空矩阵情况
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
int m = matrix.length;
int n = matrix[0].length; // 如果matrix为空,这里会抛出异常
// ...
}
// 正确写法:先检查矩阵是否为空
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return result;
}
// ...
}
方向数组使用不当:
在使用方向数组模拟法时,方向的顺序或更新逻辑错误。
// 错误写法:方向数组定义错误
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; // 顺序错误
// 正确写法:按照顺时针顺序定义方向
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右、下、左、上
减少边界检查:
在一些实现中,可以减少不必要的边界检查,提高代码执行效率。
// 优化前:每次遍历都检查边界条件
if (top <= bottom) {
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
}
// 优化后:使用循环条件隐含边界检查
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++;
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
// 检查是否还有元素未被遍历
if (top <= bottom) {
// 从右到左遍历下边界
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--;
}
if (left <= right) {
// 从下到上遍历左边界
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++;
}
}
预分配结果列表的容量:
如果知道矩阵的大小,可以预先分配结果列表的容量,减少扩容操作。
// 优化前:动态扩容
List<Integer> result = new ArrayList<>();
// 优化后:预分配容量
List<Integer> result = new ArrayList<>(m * n);
使用数组而非列表作为中间结果:
如果对性能要求极高,可以使用数组而非列表作为中间结果,避免装箱/拆箱操作和动态扩容的开销。
// 优化前:使用List
List<Integer> result = new ArrayList<>();
// 优化后:使用数组
int[] result = new int[m * n];
int index = 0;
// 在遍历过程中,使用result[index++] = matrix[i][j]添加元素
避免使用额外空间:
在方向数组模拟法中,可以考虑使用原矩阵本身来标记已访问的元素,而不是使用额外的visited数组,但这会修改原矩阵。
// 优化前:使用额外的visited数组
boolean[][] visited = new boolean[m][n];
// 优化后:直接修改原矩阵(如果允许)
// 可以使用一个特殊值(如Integer.MIN_VALUE)标记已访问的元素
简化层次遍历的条件判断:
在层次遍历法中,可以采用更简洁的条件判断方式。
// 优化前:每条边都单独判断是否需要遍历
if (topRow < bottomRow) {
// 遍历下边
}
if (leftCol < rightCol) {
// 遍历左边
}
// 优化后:使用统一的循环条件控制
while (topRow <= bottomRow && leftCol <= rightCol) {
// 遍历上边(必须执行)
for (int j = leftCol; j <= rightCol; j++) {
result.add(matrix[topRow][j]);
}
topRow++;
// 如果还有行未遍历,则遍历右边
if (topRow <= bottomRow) {
for (int i = topRow; i <= bottomRow; i++) {
result.add(matrix[i][rightCol]);
}
rightCol--;
} else {
break; // 提前结束循环
}
// 如果还有列未遍历,则遍历下边
if (leftCol <= rightCol) {
for (int j = rightCol; j >= leftCol; j--) {
result.add(matrix[bottomRow][j]);
}
bottomRow--;
} else {
break; // 提前结束循环
}
// 如果还有行未遍历,则遍历左边
if (topRow <= bottomRow) {
for (int i = bottomRow; i >= topRow; i--) {
result.add(matrix[i][leftCol]);
}
leftCol++;
} else {
break; // 提前结束循环
}
}
这些优化通常适用于大型矩阵或需要高性能的场景。对于普通的面试场景,解法一或解法三已经足够高效。
LeetCode 59. 螺旋矩阵 II:
给定一个正整数 n,生成一个包含 1 到 n² 所有元素,且元素按顺时针螺旋排列的 n x n 正方形矩阵。
这是螺旋矩阵的逆问题,需要按照螺旋顺序填充矩阵,而不是遍历矩阵。解法思路与螺旋矩阵相似,只是将读取操作变为写入操作。
LeetCode 885. 螺旋矩阵 III:
在 R 行 C 列的矩阵上,我们从 (r0, c0) 开始,顺时针螺旋行走,每次行走到矩阵中的未访问过的单元格。返回访问过的单元格坐标的列表。
LeetCode 2326. 矩阵中的不可行路径:
给你一个 m x n 的矩阵和一个链表头节点 head,按照螺旋顺序访问矩阵,同时将链表中的值依次填入矩阵。
LeetCode 74. 搜索二维矩阵:
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。虽然不是螺旋遍历,但也涉及矩阵的有效搜索。
LeetCode 48. 旋转图像:
给定一个 n × n 的二维矩阵表示一个图像,将图像顺时针旋转 90 度。这与螺旋矩阵的顺时针遍历有关联。
LeetCode 240. 搜索二维矩阵 II:
编写一个高效的算法来搜索 m x n 矩阵中的一个目标值,矩阵具有特定的排序特性。
螺旋矩阵算法在实际应用中有多种用途:
图像处理:
数据可视化:
芯片设计:
机器人路径规划:
游戏开发:
数学教育:
以下是结合了各种优化和最佳实践的完整解决方案:
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
// 处理边界情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return new ArrayList<>();
}
int m = matrix.length;
int n = matrix[0].length;
List<Integer> result = new ArrayList<>(m * n); // 预分配容量
// 定义四个边界
int top = 0;
int right = n - 1;
int bottom = m - 1;
int left = 0;
// 循环遍历,直到所有元素都被访问
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) {
result.add(matrix[top][j]);
}
top++; // 上边界下移
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--; // 右边界左移
// 检查是否还有行未被遍历
if (top <= bottom) {
// 从右到左遍历下边界
for (int j = right; j >= left; j--) {
result.add(matrix[bottom][j]);
}
bottom--; // 下边界上移
}
// 检查是否还有列未被遍历
if (left <= right) {
// 从下到上遍历左边界
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++; // 左边界右移
}
}
return result;
}
}
此解决方案使用四边界模拟法,具有以下特点:
该解决方案在LeetCode上的表现非常好,执行时间和内存使用都处于优秀水平。
理解螺旋遍历的基本模式:
边界处理的重要性:
条件检查的必要性:
解法选择的考虑因素:
通过学习螺旋矩阵问题,你可以掌握:
如果在面试中遇到此类问题:
螺旋矩阵是一个经典问题,掌握其解法不仅能够应对面试,还能帮助理解更复杂的矩阵操作问题。