螺旋矩阵
在 LeetCode 上查看 ↗语音讲解
开车或通勤时可听,跟着思路走一遍
题目描述
给你一个 m 行 n 列的矩阵 matrix,请按照 顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1
示例 2
模拟答题者思考
1. 最直接:按螺旋路径手写坐标变化——从 (0,0) 出发,方向依次为右、下、左、上,遇边界或已访问格就转向;需要 visited[m][n] 防重复,时间 O(mn),额外空间 O(mn)。
2. 重复在哪里?方向数组解法每走一步都要判「是否出界 / 是否已访问」,逻辑分散在四个分支里,单行或单列时特别容易多走或漏走。
3. 关键转化:把螺旋看成一圈圈剥洋葱——每一圈固定走四条边:顶行从左到右、右列从上到下、底行从右到左(若还有多行)、左列从下到上(若还有多列),然后四条边界各向内缩 1。
4. 手推 3×3:第一圈收集 1,2,3 → 6,9 → 8,7 → 4;收缩后只剩中心 5,第二圈顶行单独收集 5,得到 [1,2,3,6,9,8,7,4,5]。
5. 边界条件:底行仅在 top < bottom 时遍历(避免与顶行重复);左列仅在 left < right 时遍历(避免与右列重复)。单行或单列矩阵靠这两条判断自然处理。
变量语义(先读这三句再编码)
| 变量 | 类型 | 语义(三句法) |
|---|---|---|
ans | list<int> | 定义:按顺时针螺旋顺序收集到的所有元素 维护:每走完一条边,就把该边上尚未访问的元素依次追加到 ans更新:四条边(上→右→下→左)各扫一遍后, ans 长度增加当前「剩余矩形」的周长对应元素数 |
top, bottom | int | 定义:当前待遍历子矩阵的上、下边界行号(含端点) 维护:每完成一圈螺旋后, top++、bottom--,向内收缩一行更新:初始 top=0, bottom=m-1;当 top > bottom 时纵向已无剩余行,停止 |
left, right | int | 定义:当前待遍历子矩阵的左、右边界列号(含端点) 维护:每完成一圈螺旋后, left++、right--,向内收缩一列更新:初始 left=0, right=n-1;当 left > right 时横向已无剩余列,停止 |
i, j | int | 定义:沿当前边扫描时的行、列下标 维护:上边从左到右、右边从上到下、下边从右到左、左边从下到上,各用一层 for 推进更新:每访问 matrix[i][j] 后立即 ans.append(...),避免重复访问 |
落码步骤
1. 若 matrix 为空直接返回 [];取 m, n,初始化 top=0, bottom=m-1, left=0, right=n-1 与空列表 ans
2. 当 top <= bottom 且 left <= right 时循环(当前还有未访问的子矩形)
3. 上边:for j in range(left, right+1),收集 matrix[top][j]
4. 右边:for i in range(top+1, bottom+1),收集 matrix[i][right]
5. 若 top < bottom,下边从 right-1 到 left 逆序收集 matrix[bottom][j]
6. 若 left < right,左边从 bottom-1 到 top+1 逆序收集 matrix[i][left]
7. 收缩边界 top++, bottom--, left++, right--,进入下一圈
8. 返回 ans
代码实现
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix or not matrix[0]:
return []
m, n = len(matrix), len(matrix[0])
top, bottom = 0, m - 1
left, right = 0, n - 1
ans: list[int] = []
while top <= bottom and left <= right:
# 上边:从左到右
for j in range(left, right + 1):
ans.append(matrix[top][j])
# 右边:从上到下(跳过顶角,已在上边收集)
for i in range(top + 1, bottom + 1):
ans.append(matrix[i][right])
# 下边:从右到左(仅当还有多行时)
if top < bottom:
for j in range(right - 1, left - 1, -1):
ans.append(matrix[bottom][j])
# 左边:从下到上(仅当还有多列时)
if left < right:
for i in range(bottom - 1, top, -1):
ans.append(matrix[i][left])
top += 1
bottom -= 1
left += 1
right -= 1
return ans
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
if (matrix.empty() || matrix[0].empty()) return ans;
int m = matrix.size(), n = matrix[0].size();
int top = 0, bottom = m - 1, left = 0, right = n - 1;
while (top <= bottom && left <= right) {
// 上边:从左到右
for (int j = left; j <= right; j++)
ans.push_back(matrix[top][j]);
// 右边:从上到下
for (int i = top + 1; i <= bottom; i++)
ans.push_back(matrix[i][right]);
// 下边:从右到左(仅当还有多行)
if (top < bottom) {
for (int j = right - 1; j >= left; j--)
ans.push_back(matrix[bottom][j]);
}
// 左边:从下到上(仅当还有多列)
if (left < right) {
for (int i = bottom - 1; i > top; i--)
ans.push_back(matrix[i][left]);
}
top++;
bottom--;
left++;
right--;
}
return ans;
}
};
// 时间 O(m×n),空间 O(1)(不计输出)
复杂度分析
O(m × n)
O(1)(不计输出数组)
常见坑
忘记「单行/单列」判断:走完顶行和右列后,若 top == bottom 仍遍历底行,会把同一行元素重复加入;必须用 if (top < bottom) 和 if (left < right) 保护。
方向数组 + visited 写法里,转向时机写错会导致死循环或漏元素;剥洋葱法用边界收缩,每格恰好访问一次,更不易错。
右边循环应从 top+1 开始、左边从 bottom-1 到 top+1,否则四个角的元素会被重复收集。
必测边界 Case
matrix = [[1,2,3,4]] → [1,2,3,4](只走顶边,top==bottom 跳过底边)
matrix = [[1],[2],[3]] → [1,2,3](顶边后只走右列,left==right 跳过左边)
matrix = [[7]] → [7](一圈只收集一个元素)
matrix = [[1,2,3],[4,5,6],[7,8,9]] → [1,2,3,6,9,8,7,4,5](中心 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](非方阵同样适用边界收缩)