LeetCode 54:螺旋矩阵------用四个边界模拟矩阵收缩
一、题目描述
今天做的是 LeetCode 54:螺旋矩阵。
题目要求按照顺时针螺旋顺序,遍历矩阵中的所有元素。
例如:

text
1 2 3
4 5 6
7 8 9
按照顺时针方向遍历:
text
1 → 2 → 3
↓
4 → 5 → 6
↑ ↓
7 ← 8 ← 9
最终结果:
text
[1,2,3,6,9,8,7,4,5]

这道题如果直接按照"向右、向下、向左、向上"不断移动坐标来做,比较容易把边界条件搞复杂。
所以我的思路是:
不去记录每一个位置的移动方向,而是直接维护当前还没有遍历的矩形区域。
二、我的思路:维护四个边界
我定义四个变量:
java
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
它们分别表示当前还没有遍历区域的:
text
top → 上边界
bottom → 下边界
left → 左边界
right → 右边界
例如:
text
┌───────────────┐
│ top │
│ │
│ │
│ │
│ bottom │
└───────────────┘
↑ ↑
left right
每完成一条边的遍历,就把对应的边界向里面缩一格。
所以整个过程实际上就是:
text
遍历上边
↓
top++
遍历右边
↓
right--
遍历下边
↓
bottom--
遍历左边
↓
left++
不断缩小范围,直到:
java
top > bottom
或者:
java
left > right
三、按照"右 → 下 → 左 → 上"依次遍历
代码:
java
while (top <= bottom && left <= right) {
// 从左到右遍历上边
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++;
// 从上到下遍历右边
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
// 从右到左遍历下边
for (int i = right; i >= left && top <= bottom; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
// 从下到上遍历左边
for (int i = bottom; i >= top && left <= right; i--) {
result.add(matrix[i][left]);
}
left++;
}
整个循环其实非常有规律:
text
① 左 → 右
② 上 → 下
③ 右 → 左
④ 下 → 上
然后重复。
每一轮都会把外面的一圈处理掉。
四、为什么遍历完一条边就要修改边界?
比如第一步:
java
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++;
我们已经把最上面这一行遍历完成:
text
1 2 3
4 5 6
7 8 9
此时:
text
1 2 3
------
4 5 6
7 8 9
第一行已经处理过了,所以:
java
top++;
让 top 从:
text
0 → 1
这样下一轮就不会再次访问这一行。
其他三个边界也是一样:
text
上边遍历完 → top++
右边遍历完 → right--
下边遍历完 → bottom--
左边遍历完 → left++
可以把它理解成:
每遍历完一条边,就把这条边从"未处理区域"中删除。
五、为什么后面两个 for 要额外判断边界?
这是这道题最容易出问题的地方。
看一个只有一行的矩阵:
text
1 2 3 4
第一步:
java
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
得到:
text
1 2 3 4
然后:
java
top++;
此时:
text
top = 1
bottom = 0
已经满足:
text
top > bottom
说明矩阵已经遍历完成。
但是如果没有判断,下面的代码仍然可能继续访问:
java
matrix[bottom][i]
这样就会把已经访问过的数据再次加入结果。
所以我的代码:
java
for (int i = right; i >= left && top <= bottom; i--) {
result.add(matrix[bottom][i]);
}
这里:
java
top <= bottom
就是在确认:
下面这一行是否真的还存在。
同样,遍历左边的时候:
java
for (int i = bottom; i >= top && left <= right; i--) {
result.add(matrix[i][left]);
}
这里:
java
left <= right
是在确认:
左边这一列是否还没有被处理。
这两个判断非常重要,因为矩阵不一定是正方形。
可能是:
text
1 × n
也可能是:
text
m × 1
还可能在最后一圈只剩下一行或者一列。
六、用 3 × 3 矩阵完整走一遍
例如:
text
1 2 3
4 5 6
7 8 9
初始:
text
top = 0
bottom = 2
left = 0
right = 2
第一圈
从左到右:
text
1 2 3
然后:
text
top = 1
从上到下:
text
6
9
然后:
text
right = 1
从右到左:
text
8 7
然后:
text
bottom = 1
从下到上:
text
4
然后:
text
left = 1
此时剩下:
text
5
也就是:
text
top = 1
bottom = 1
left = 1
right = 1
继续下一轮:
text
5
最终:
text
[1,2,3,6,9,8,7,4,5]
七、我的最终代码
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 左 → 右
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++;
// 上 → 下
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
// 右 → 左
for (int i = right; i >= left && top <= bottom; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
// 下 → 上
for (int i = bottom; i >= top && left <= right; i--) {
result.add(matrix[i][left]);
}
left++;
}
return result;
}
}
八、复杂度分析
虽然代码中有四个 for 循环,但并不是:
text
O(n⁴)
或者其他更高的复杂度。
因为每一个矩阵元素最终只会被加入 result 一次。
假设矩阵共有:
text
m × n
个元素,那么所有元素最多访问一次。
因此:
text
时间复杂度:O(m × n)
结果本身就需要保存所有元素,所以 result 需要:
text
O(m × n)
的空间。
如果不计算返回结果本身占用的空间,那么算法额外使用的辅助空间实际上是:
text
O(1)
九、这道题的核心
这道题表面上是在考:
如何按照顺时针方向遍历矩阵。
但我觉得真正值得记住的是边界收缩。
不需要真的让一个坐标:
text
向右走
↓
向下走
↓
向左走
↓
向上走
而是直接把问题变成:
text
当前矩形区域
↓
遍历上边
↓
缩小 top
遍历右边
↓
缩小 right
遍历下边
↓
缩小 bottom
遍历左边
↓
缩小 left
重复
最终:
text
top > bottom
或者
left > right
就说明所有元素都已经处理完成。
十、总结
这道题我采用的是四边界模拟法。
核心就是维护:
java
top
bottom
left
right
然后按照:
text
左 → 右
上 → 下
右 → 左
下 → 上
的顺序遍历。
每遍历完一条边,就把对应边界向内收缩。
其中最需要注意的是最后两次遍历:
java
top <= bottom
和:
java
left <= right
它们是为了防止矩阵在最后只剩下一行或一列时,重复遍历已经访问过的元素。
最终复杂度:
text
时间复杂度:O(m × n)
额外空间复杂度:O(1)
这道题让我对矩阵遍历有了一个比较实用的思路:
当遍历顺序具有明显的"外圈 → 内圈"特征时,可以考虑用边界不断向内收缩,而不是直接模拟坐标移动。