LeetCode 剑指 Offer 29. 顺时针打印矩阵
题目描述



题意分析
从矩阵左上角出发,按向右、向下、向左、向上的顺序沿外圈读取,再继续读取内圈,直到所有位置都访问一次。返回按访问顺序排列的一维数组,不需要修改矩阵。
矩阵不一定是方阵,可能为空,也可能只剩一行或一列。相同数值出现在不同位置时仍要分别输出,去重针对的是位置是否重复访问,而不是数值。
解法:四边界模拟
核心思路
[!blue]
每读完一圈,未访问的部分仍然是一个矩形,所以不必逐格记录是否访问。用闭区间
[top, bottom]和[left, right]描述剩余矩形,每轮按顺时针方向剥去四条边。先从左到右读取
top行,然后top++;右边从更新后的top开始向下读取,因此不会重复右上角。接着right--,下边再从更新后的right向左读取,便不会重复右下角。同理,读完下边后bottom--,左边从新的bottom向上读取,最后left++。收缩途中可能已经没有剩余行或列。读下边前需要确认
top <= bottom,读左边前需要确认left <= right;另一方向的循环上下界则自然使已空区间不执行。这样最后只有一行或一列时,也不会反向再读一遍。一轮结束后,四条边各自只处理一次,内层剩余矩形继续由四个边界完整表示。只要行边界或列边界交错,所有位置就已经输出,整个过程既不重复也不遗漏。
解题步骤
- 先判断空矩阵或空行,直接返回空数组,避免访问不存在的第一行。
- 将四个边界初始化为整个矩阵,准备容量为元素总数的结果容器。
- 当
top <= bottom且left <= right时,读取上边并执行top++,再读取右边并执行right--。- 若仍有行,反向读取下边并上移
bottom;若仍有列,向上读取左边并右移left。- 重复收缩直到边界交错,返回结果。
代码实现
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return new int[0];
}
int rows = matrix.length;
int cols = matrix[0].length;
int[] res = new int[rows * cols];
int idx = 0;
int top = 0;
int bottom = rows - 1;
int left = 0;
int right = cols - 1;
while (top <= bottom && left <= right) {
for (int col = left; col <= right; col++) {
res[idx++] = matrix[top][col];
}
// 读完一边立即收缩,下一边从尚未访问的位置开始。
top++;
for (int row = top; row <= bottom; row++) {
res[idx++] = matrix[row][right];
}
right--;
// 最后可能只剩一行,收缩后先确认下边仍然存在。
if (top <= bottom) {
for (int col = right; col >= left; col--) {
res[idx++] = matrix[bottom][col];
}
bottom--;
}
// 最后可能只剩一列,收缩后先确认左边仍然存在。
if (left <= right) {
for (int row = bottom; row >= top; row--) {
res[idx++] = matrix[row][left];
}
left++;
}
}
return res;
}
}
func spiralOrder(matrix [][]int) []int {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return []int{}
}
rows := len(matrix)
cols := len(matrix[0])
res := make([]int, 0, rows*cols)
top := 0
bottom := rows - 1
left := 0
right := cols - 1
for top <= bottom && left <= right {
for col := left; col <= right; col++ {
res = append(res, matrix[top][col])
}
// 读完一边立即收缩,下一边从尚未访问的位置开始。
top++
for row := top; row <= bottom; row++ {
res = append(res, matrix[row][right])
}
right--
// 最后可能只剩一行,收缩后先确认下边仍然存在。
if top <= bottom {
for col := right; col >= left; col-- {
res = append(res, matrix[bottom][col])
}
bottom--
}
// 最后可能只剩一列,收缩后先确认左边仍然存在。
if left <= right {
for row := bottom; row >= top; row-- {
res = append(res, matrix[row][left])
}
left++
}
}
return res
}
复杂度分析
- 时间复杂度:$O(mn)$,每个格子恰好写入结果一次。
- 空间复杂度:$O(1)$,只使用四个边界和结果下标;若计入返回数组则为 $O(mn)$。
关键点总结
[!green]
- 四边界是“未访问矩形”的最小充分状态,比
visited数组更省空间。- 每输出一条边就立即收缩对应边界,下一条边从收缩后的端点开始,避免重复角点。
- 下边与左边读取前必须重新判断边界,这是单行、单列正确性的关键。
易错点总结
[!yellow]
- 未判空就访问
matrix[0]:matrix=[]会直接越界。- 下边读取前不检查
top <= bottom:单行矩阵会正向、反向各输出一次。- 左边读取前不检查
left <= right:单列矩阵会被重复输出。- 右边从旧
top开始:右上角会与上边重复;后续边同理要避开已处理角点。- 输出后忘记收缩边界:下一轮会重复同一圈,甚至死循环。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 59. 螺旋矩阵 II | 中等 | 螺旋边界更新相同,本题读取矩阵,原题按顺序把数写入矩阵。 |
| 885. 螺旋矩阵 III | 中等 | 同样按方向轮转,本题逐层收缩矩形边界,原题允许走出矩阵再进入。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!