刷LeetCode数组类题目时,「螺旋遍历」绝对是高频且易出错的考点——看似简单的顺时针顺序,实则需要精准控制遍历方向和边界,稍有不慎就会出现索引越界、元素遗漏或重复的问题。
本文针对 LeetCode 54. 螺旋矩阵 题目,详细拆解两种主流解法(方向标记法+边界收缩法),结合完整可运行代码,帮你彻底搞懂螺旋遍历的核心逻辑,刷题时再也不慌!
一、题目核心解析
题目描述
给你一个 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]
核心难点
-
控制遍历方向:顺时针(右→下→左→上)的循环切换,不能出现方向错乱;
-
处理边界收缩:每遍历完一圈(或一条边),边界需要向内收缩,避免重复遍历;
-
规避边界异常:空矩阵、单行矩阵(m=1)、单列矩阵(n=1)等特殊场景,需单独处理,防止索引越界。
二、解法一:方向标记法(易懂易上手)
解法思路
核心逻辑:用一个「方向标记 dir」控制遍历方向(1=右、2=下、3=左、4=上),用「圈数标记 laps」控制边界收缩,遍历过程中不断更新当前坐标(row, col),到达边界时切换方向,完成一圈后收缩边界(laps++),直到收集完所有元素。
特殊处理:先单独处理空矩阵、单行矩阵、单列矩阵,简化核心遍历逻辑,避免无效的方向切换。
完整可运行代码
function spiralOrder_1(matrix: number[][]): number[] {
// 处理空矩阵/每行空数组的边界情况
if (matrix.length === 0 || (matrix[0] && matrix[0].length === 0)) {
return [];
}
const m = matrix.length; // 矩阵行数
const n = matrix[0].length; // 矩阵列数
// 单独处理单行矩阵:直接返回第一行(螺旋顺序就是自身顺序)
if (m === 1) {
return matrix[0];
}
// 单独处理单列矩阵:从上到下收集每一行的第0列元素
if (n === 1) {
const res = [];
for (let row = 0; row < m; row++) {
res.push(matrix[row][0]);
}
return res;
}
let row = 0; // 当前行索引
let col = 0; // 当前列索引
let dir = 1; // 遍历方向(1:右 2:下 3:左 4:上)
let laps = 0; // 已遍历圈数(用于收缩边界)
const res = []; // 存储结果的数组
// 循环终止条件:收集完所有元素(res长度等于矩阵元素总数m*n)
while (res.length < m * n) {
// 先将当前位置元素存入结果
res.push(matrix[row][col]);
// 根据方向更新坐标,到达边界则切换方向
switch (dir) {
case 1: // 方向1:向右遍历
col++;
// 到达当前圈最右列(n-1-laps),切换为向下
if (col === n - 1 - laps) {
dir = 2;
}
break;
case 2: // 方向2:向下遍历
row++;
// 到达当前圈最下行(m-1-laps),切换为向左
if (row === m - 1 - laps) {
dir = 3;
}
break;
case 3: // 方向3:向左遍历
col--;
// 到达当前圈最左列(laps),切换为向上
if (col === laps) {
dir = 4;
}
break;
case 4: // 方向4:向上遍历
row--;
// 到达当前圈最上行(laps+1),完成一圈,收缩边界并切换为向右
if (row === laps + 1) {
laps++;
dir = 1;
}
break;
}
}
return res;
};
关键逻辑拆解
-
边界预处理:优先判断空矩阵、单行、单列矩阵,避免后续遍历逻辑出错,同时简化代码;
-
方向切换规则:右→下(最右列)、下→左(最下行)、左→上(最左列)、上→右(最上行),循环往复;
-
边界收缩:每完成一圈(向上遍历到 laps+1 行),laps 加 1,此时下一圈的边界会向内收缩 1(比如最右列变为 n-1-(laps+1));
-
终止条件:res.length === m*n,确保所有元素都被收集,避免无限循环。
优缺点分析
✅ 优点:逻辑直观,方向切换清晰,适合新手理解;特殊场景单独处理,代码可读性高;
❌ 缺点:依赖 dir 和 laps 两个标记,边界判断较多,若方向与边界对应错误,容易出现越界(后续会说如何规避)。
三、解法二:边界收缩法(稳健高效,推荐)
这是螺旋矩阵最主流、最稳健的解法——无需方向标记,直接用四个变量(top、bottom、left、right)框定当前未遍历的矩阵区域,按「左→右、上→下、右→左、下→上」的顺序遍历区域边界,每遍历完一条边就收缩对应边界,直到所有元素被收集。
完整可运行代码
function spiralOrder_2(matrix: number[][]): number[] {
// 第一步:处理空矩阵/无效矩阵(彻底避免访问 matrix[0].length 报错)
if (!matrix || matrix.length === 0 || !matrix[0] || matrix[0].length === 0) {
return [];
}
const m = matrix.length; // 矩阵的行数(绝对行边界:0 ~ m-1)
const n = matrix[0].length; // 矩阵的列数(绝对列边界:0 ~ n-1)
let top = 0; // 上边界(当前未遍历的最上行)
let bottom = m - 1; // 下边界(当前未遍历的最下行)
let left = 0; // 左边界(当前未遍历的最左列)
let right = n - 1; // 右边界(当前未遍历的最右列)
const res: number[] = []; // 存储结果的数组
// 循环条件:结果数组的长度小于矩阵元素总数
while (res.length < m * n) {
// 1. 从左到右遍历上边界行(top 行)
if (top <= bottom && left <= right) { // 先判断边界相对合法,避免无效遍历
for (let col = left; col <= right && res.length < m * n; col++) {
// 索引绝对合法校验(兜底,避免越界)
if (top >= 0 && top < m && col >= 0 && col < n) {
res.push(matrix[top][col]);
}
}
top++; // 上边界向下收缩一行(当前top行已遍历完成)
}
// 2. 从上到下遍历右边界列(right 列)
if (left <= right && top <= bottom) { // 先判断边界相对合法
for (let row = top; row <= bottom && res.length < m * n; row++) {
// 索引绝对合法校验(兜底,避免越界)
if (row >= 0 && row < m && right >= 0 && right < n) {
res.push(matrix[row][right]);
}
}
right--; // 右边界向左收缩一列(当前right列已遍历完成)
}
// 3. 从右到左遍历下边界行(bottom 行)
if (bottom >= top && right >= left) { // 先判断边界相对合法
for (let col = right; col >= left && res.length < m * n; col--) {
// 索引绝对合法校验(兜底,避免越界)
if (bottom >= 0 && bottom < m && col >= 0 && col < n) {
res.push(matrix[bottom][col]);
}
}
bottom--; // 下边界向上收缩一行(当前bottom行已遍历完成)
}
// 4. 从下到上遍历左边界列(left 列)
if (right >= left && bottom >= top) { // 先判断边界相对合法
for (let row = bottom; row >= top && res.length < m * n; row--) {
// 索引绝对合法校验(兜底,彻底避免 row 越界导致 matrix[row] 为 undefined)
if (row >= 0 && row < m && left >= 0 && left< n) {
res.push(matrix[row][left]);
}
}
left++; // 左边界向右收缩一列(当前left列已遍历完成)
}
}
return res;
};
关键逻辑拆解
-
边界初始化:top=0(最上行)、bottom=m-1(最下行)、left=0(最左列)、right=n-1(最右列),框定整个矩阵;
-
四轮遍历为一圈:
-
左→右:遍历 top 行,从 left 列到 right 列,遍历完 top++(上边界收缩);
-
上→下:遍历 right 列,从 top 行到 bottom 行,遍历完 right--(右边界收缩);
-
右→左:遍历 bottom 行,从 right 列到 left 列,遍历完 bottom--(下边界收缩);
-
下→上:遍历 left 列,从 bottom 行到 top 行,遍历完 left++(左边界收缩);
- 双重边界校验:
-
相对校验(如 top <= bottom):确保当前边界有可遍历元素,避免无效循环;
-
绝对校验(如 row >=0 && row < m):兜底保障,彻底避免索引越界(解决报错核心);
- 终止条件:同解法一,res.length === m*n 时停止,确保不遗漏、不重复。
优缺点分析
✅ 优点:稳健性极强,无方向标记,边界逻辑清晰,不易出错;无需单独处理单行/单列矩阵(双重校验已覆盖);
❌ 缺点:边界判断较多,初期理解起来比方向标记法稍复杂,但掌握后可应对所有场景。
四、两种解法对比 & 实战建议
| 对比维度 | 解法一:方向标记法 | 解法二:边界收缩法 |
|---|---|---|
| 核心逻辑 | 方向标记+圈数收缩 | 四边界框定+逐边收缩 |
| 可读性 | 高(方向清晰,适合新手) | 中(边界校验多,需理解收缩逻辑) |
| 稳健性 | 中(需注意方向与边界对应) | 高(双重校验,无越界风险) |
| 代码复杂度 | 低(特殊场景单独处理,核心逻辑简洁) | 中(边界校验多,但无需单独处理特殊场景) |
| 适用场景 | 新手入门、简单矩阵场景 | 面试实战、复杂边界场景(推荐) |
实战建议
-
新手优先学「方向标记法」:先理解螺旋遍历的方向切换和边界收缩逻辑,上手快,不容易产生挫败感;
-
面试推荐用「边界收缩法」:稳健性强,不易出错,面试官更认可这种通用解法(可应对所有特殊场景);
-
避坑重点:无论哪种解法,都要先处理空矩阵,且必须做好索引越界校验——这是螺旋矩阵报错的核心原因(如本文开头遇到的 undefined 报错)。
六、总结
LeetCode 54. 螺旋矩阵的核心,本质是「边界控制」——无论是方向标记法还是边界收缩法,核心都是通过合理的边界管理,实现顺时针遍历且不重复、不遗漏、不越界。
两种解法各有优势:方向标记法易懂,适合入门;边界收缩法稳健,适合实战。建议先吃透方向标记法的逻辑,再过渡到边界收缩法,掌握后就能轻松应对这类螺旋遍历问题。