LeetCode 54. 螺旋矩阵:两种解法吃透顺时针遍历逻辑

0 阅读8分钟

刷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]

核心难点

  1. 控制遍历方向:顺时针(右→下→左→上)的循环切换,不能出现方向错乱;

  2. 处理边界收缩:每遍历完一圈(或一条边),边界需要向内收缩,避免重复遍历;

  3. 规避边界异常:空矩阵、单行矩阵(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;
};

关键逻辑拆解

  1. 边界预处理:优先判断空矩阵、单行、单列矩阵,避免后续遍历逻辑出错,同时简化代码;

  2. 方向切换规则:右→下(最右列)、下→左(最下行)、左→上(最左列)、上→右(最上行),循环往复;

  3. 边界收缩:每完成一圈(向上遍历到 laps+1 行),laps 加 1,此时下一圈的边界会向内收缩 1(比如最右列变为 n-1-(laps+1));

  4. 终止条件: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;
};

关键逻辑拆解

  1. 边界初始化:top=0(最上行)、bottom=m-1(最下行)、left=0(最左列)、right=n-1(最右列),框定整个矩阵;

  2. 四轮遍历为一圈:

  • 左→右:遍历 top 行,从 left 列到 right 列,遍历完 top++(上边界收缩);

  • 上→下:遍历 right 列,从 top 行到 bottom 行,遍历完 right--(右边界收缩);

  • 右→左:遍历 bottom 行,从 right 列到 left 列,遍历完 bottom--(下边界收缩);

  • 下→上:遍历 left 列,从 bottom 行到 top 行,遍历完 left++(左边界收缩);

  1. 双重边界校验:
  • 相对校验(如 top <= bottom):确保当前边界有可遍历元素,避免无效循环;

  • 绝对校验(如 row >=0 && row < m):兜底保障,彻底避免索引越界(解决报错核心);

  1. 终止条件:同解法一,res.length === m*n 时停止,确保不遗漏、不重复。

优缺点分析

✅ 优点:稳健性极强,无方向标记,边界逻辑清晰,不易出错;无需单独处理单行/单列矩阵(双重校验已覆盖);

❌ 缺点:边界判断较多,初期理解起来比方向标记法稍复杂,但掌握后可应对所有场景。

四、两种解法对比 & 实战建议

对比维度解法一:方向标记法解法二:边界收缩法
核心逻辑方向标记+圈数收缩四边界框定+逐边收缩
可读性高(方向清晰,适合新手)中(边界校验多,需理解收缩逻辑)
稳健性中(需注意方向与边界对应)高(双重校验,无越界风险)
代码复杂度低(特殊场景单独处理,核心逻辑简洁)中(边界校验多,但无需单独处理特殊场景)
适用场景新手入门、简单矩阵场景面试实战、复杂边界场景(推荐)

实战建议

  1. 新手优先学「方向标记法」:先理解螺旋遍历的方向切换和边界收缩逻辑,上手快,不容易产生挫败感;

  2. 面试推荐用「边界收缩法」:稳健性强,不易出错,面试官更认可这种通用解法(可应对所有特殊场景);

  3. 避坑重点:无论哪种解法,都要先处理空矩阵,且必须做好索引越界校验——这是螺旋矩阵报错的核心原因(如本文开头遇到的 undefined 报错)。

六、总结

LeetCode 54. 螺旋矩阵的核心,本质是「边界控制」——无论是方向标记法还是边界收缩法,核心都是通过合理的边界管理,实现顺时针遍历且不重复、不遗漏、不越界。

两种解法各有优势:方向标记法易懂,适合入门;边界收缩法稳健,适合实战。建议先吃透方向标记法的逻辑,再过渡到边界收缩法,掌握后就能轻松应对这类螺旋遍历问题。