螺旋矩阵:四方向边界收缩的工程实现
螺旋矩阵:四方向边界收缩的工程实现
适读人群:Java 开发者、准备算法面试的工程师 | 阅读时长:约18分钟 | 难度:Medium
开篇故事
螺旋矩阵是我见过的最让初学者头疼的模拟题之一。不是因为算法复杂,而是因为边界处理稍不小心就出一堆 bug。我在面试别人的时候出过这道题,有个候选人说:"我知道怎么做,就是写代码的时候老是差一格。"
差一格,这是这道题最典型的症状。根源在于没有一个清晰的边界管理策略,靠直觉推断边界条件,自然错误不断。
我自己第一次做这道题用的是"方向数组"方案,每次遇到边界就顺时针转向,逻辑清晰但代码稍长。后来改成了"四边界收缩"方案,代码更短,也更容易理解和验证。今天两种方案都写出来,你选自己更顺手的那种。
一、题目解析
LeetCode 54. 螺旋矩阵(Spiral Matrix)
给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
输入输出示例:
示例 1(3×3):
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]示例 2(3×4):
输入: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]边界情况 3(1行):
输入:matrix = [[1,2,3]]
输出:[1,2,3]边界情况 4(1列):
输入:matrix = [[1],[2],[3]]
输出:[1,2,3]边界情况 5(1×1):
输入:matrix = [[5]]
输出:[5]考察的核心知识点:
- 四方向遍历(右→下→左→上循环)
- 边界收缩策略
- 边界条件的精确控制(避免重复访问)
二、解法一:方向数组+状态判断
思路分析
维护当前方向(右/下/左/上),用方向数组 dirs = [{0,1},{1,0},{0,-1},{-1,0}}(右下左上)。遇到边界或已访问位置就换方向。用 visited 数组标记已访问位置。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[][] visited = new boolean[m][n];
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右下左上
int dir = 0; // 当前方向
int r = 0, c = 0;
List<Integer> result = new ArrayList<>();
for (int i = 0; i < m * n; i++) {
result.add(matrix[r][c]);
visited[r][c] = true;
int nr = r + dirs[dir][0];
int nc = c + dirs[dir][1];
// 如果下一个位置越界或已访问,换方向
if (nr < 0 || nr >= m || nc < 0 || nc >= n || visited[nr][nc]) {
dir = (dir + 1) % 4;
nr = r + dirs[dir][0];
nc = c + dirs[dir][1];
}
r = nr;
c = nc;
}
return result;
}
}时间复杂度:O(m×n),空间复杂度:O(m×n)(visited 数组)。
三、解法二:四边界收缩(O(1) 空间,推荐)
核心洞见
不需要 visited 数组,用四个边界(top, bottom, left, right)来控制遍历范围。每遍历完一行或一列,就收缩对应的边界。
步骤:
- 向右遍历第 top 行(从 left 到 right),然后
top++ - 向下遍历第 right 列(从 top 到 bottom),然后
right-- - 向左遍历第 bottom 行(从 right 到 left),然后
bottom--(注意:先检查 top <= bottom) - 向上遍历第 left 列(从 bottom 到 top),然后
left++(注意:先检查 left <= right)
步骤 3 和 4 需要额外判断,因为可能在步骤 1/2 之后已经没有剩余行/列了。
算法流程
完整 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, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 1. 向右遍历:top 行,从 left 到 right
for (int c = left; c <= right; c++) {
result.add(matrix[top][c]);
}
top++; // 上边界向下收缩
// 2. 向下遍历:right 列,从 top 到 bottom
for (int r = top; r <= bottom; r++) {
result.add(matrix[r][right]);
}
right--; // 右边界向左收缩
// 3. 向左遍历:bottom 行,从 right 到 left(需要先检查 top<=bottom)
if (top <= bottom) {
for (int c = right; c >= left; c--) {
result.add(matrix[bottom][c]);
}
bottom--; // 下边界向上收缩
}
// 4. 向上遍历:left 列,从 bottom 到 top(需要先检查 left<=right)
if (left <= right) {
for (int r = bottom; r >= top; r--) {
result.add(matrix[r][left]);
}
left++; // 左边界向右收缩
}
}
return result;
}
}复杂度分析
时间复杂度:O(m×n),每个元素恰好被访问一次。
空间复杂度:O(1)(不算输出列表)。
我在 LeetCode 提交,运行时间 0ms,击败 100% 的 Java 用户。
四、举一反三
同类型题目
LeetCode 59. 螺旋矩阵 II
按螺旋顺序填充 1 到 n² 的数字。和本题是互逆操作,同样用四边界收缩方案。
class Solution {
public int[][] generateMatrix(int n) {
int[][] matrix = new int[n][n];
int top = 0, bottom = n - 1, left = 0, right = n - 1;
int num = 1;
while (top <= bottom && left <= right) {
for (int c = left; c <= right; c++) matrix[top][c] = num++;
top++;
for (int r = top; r <= bottom; r++) matrix[r][right] = num++;
right--;
if (top <= bottom) {
for (int c = right; c >= left; c--) matrix[bottom][c] = num++;
bottom--;
}
if (left <= right) {
for (int r = bottom; r >= top; r--) matrix[r][left] = num++;
left++;
}
}
return matrix;
}
}LeetCode 885. 螺旋矩阵 III
从给定起点出发,按螺旋顺序遍历矩阵(矩阵可能不是正方形,起点可能不是 (0,0))。方向数组方案更灵活。
五、踩坑实录
坑一:向左和向上遍历时忘记额外判断
现象: 对于行数比列数多(或行数比列数少)的矩阵,输出结果有重复元素。比如 4 行 1 列的矩阵,向右遍历完一行后 top++,向下遍历完最右列后 right--,此时 right < left,但没有检查就继续向左遍历,把第一行又遍历了一遍。
原因: 向左遍历第三步和向上遍历第四步是"回程"操作,必须先检查对应维度的边界是否还合法(top <= bottom 和 left <= right)。
解法: 在步骤 3 和步骤 4 前加入边界检查,这是四边界收缩方案里最容易遗漏的细节。
坑二:边界收缩后的再次遍历用新边界
现象: 向下遍历时从 r=top 开始,但此时 top 已经是收缩之后的值,某些情况下会遗漏元素。
原因: 这其实是正确的!top 在向右遍历结束后才递增,所以向下遍历用的是更新后的 top(跳过了已经遍历的那一行),这是对的。同理,right 在向下遍历结束后递减,向左遍历用更新后的 right。
需要确认的是:每次操作后对应边界确实更新了,不会重复遍历同一行/列。
坑三:1×1 矩阵或 1 行矩阵时的处理
现象: 某些极端情况下步骤 3(向左)执行了,但 top > bottom 了,访问到了无效区域。
解法: 步骤 3 前的 if (top <= bottom) 检查完全覆盖了这个情况。1×1 矩阵时,步骤 1 遍历完 top 行后 top++,此时 top > bottom,步骤 3 和 4 均不执行,完全正确。
六、总结
螺旋矩阵的关键是边界管理,不是算法思路本身(思路简单:右→下→左→上循环)。四边界收缩方案把"什么时候结束某个方向的遍历"转化成了清晰的边界判断,避免了 visited 数组的空间开销。
三条核心要点:
四边界(top, bottom, left, right)是这道题最清晰的状态表示,每次遍历完一条边就收缩对应边界。
向左(第三步)和向上(第四步)遍历前必须检查剩余边界是否合法(top <= bottom 和 left <= right),这是最容易遗漏的地方。
每步遍历使用的起止坐标要用收缩后的最新边界值,不能用收缩前的旧值,否则会访问到已遍历区域。
延伸思考:
螺旋矩阵的变体 LeetCode 59(生成螺旋矩阵)代码结构完全一样,只是把"读取值"改为"填入值",是这道题的直接反向应用,两道题放在一起练习效果最好。
