螺旋矩阵

矩阵 模拟

题目描述

给定一个 m × n 的矩阵,按照顺时针螺旋顺序,返回矩阵中的所有元素。

思路分析

螺旋遍历的本质是一圈一圈地向内收缩。每走完一圈,当前圈的四条边就被"消耗掉",矩阵的有效区域随之缩小。用四个变量 topbottomleftright 来描述这个不断收缩的边界,就能清晰地表达这一过程。

每一轮迭代分为四个阶段:先沿着 top 行从左到右收集元素,收集完毕后将 top 下移一行;再沿着 right 列从上到下收集,完成后将 right 左移一列;接着沿着 bottom 行从右到左收集,完成后将 bottom 上移一行;最后沿着 left 列从下到上收集,完成后将 left 右移一列。

需要注意的是,后两个阶段在执行之前必须检查边界是否仍然有效。收集底部行之前,要确认 top <= bottom,即顶部边界还没有越过底部边界,否则这一行已经被之前的遍历覆盖了;同理,收集左列之前,要确认 left <= right,确保还有列没有被处理过。遗漏这两个守卫条件,在遍历非方形矩阵或只剩单行、单列时就会重复采集元素。

只要 left <= righttop <= bottom 同时成立,就说明矩阵内还有未访问的元素,循环继续;任意一个条件不满足时,遍历结束。

算法步骤

  1. 若矩阵为空,直接返回空数组。
  2. 初始化四个边界:top = 0bottom = m - 1left = 0right = n - 1
  3. 进入循环,条件为 left <= right && top <= bottom
    • 从左到右遍历 top 行,收集元素,然后 top++
    • 从上到下遍历 right 列,收集元素,然后 right--
    • top <= bottom,从右到左遍历 bottom 行,收集元素,然后 bottom--
    • left <= right,从下到上遍历 left 列,收集元素,然后 left++
  4. 返回结果数组。

示例代码

/**
 * 螺旋矩阵 —— 按顺时针螺旋顺序返回矩阵中的所有元素。
 *
 * 核心思路:用四个变量维护一个"可访问区域"的边界(上、下、左、右)。
 * 每遍历完一条边,就把该边界向内收缩一格,下一轮循环处理更小的区域,
 * 直到边界交叉(区域为空)为止。
 *
 * 想象你站在矩阵外圈,沿着 → ↓ ← ↑ 的顺序走一圈,
 * 然后走进内圈再走一圈,如此往复直到走完全部格子。
 *
 * @param matrix - m 行 n 列的整数矩阵
 * @returns 按螺旋顺序排列的所有元素
 *
 * @example
 * spiralOrder([
 *   [1, 2, 3],
 *   [4, 5, 6],
 *   [7, 8, 9],
 * ]);
 * // 返回 [1, 2, 3, 6, 9, 8, 7, 4, 5]
 */
export function spiralOrder(matrix: number[][]): number[] {
  if (!matrix || matrix.length === 0 || matrix[0].length === 0) {
    return [];
  }

  const result: number[] = [];

  // 四个边界变量描述当前"还没被访问过"的矩形区域。
  // 每遍历完一条边,对应的边界就向内收缩一格。
  let top = 0;                      // 未访问区域的最上行
  let left = 0;                     // 未访问区域的最左列
  let bottom = matrix.length - 1;   // 未访问区域的最下行
  let right = matrix[0].length - 1; // 未访问区域的最右列

  // 只要区域还存在(行和列都没有交叉),就继续遍历
  while (left <= right && top <= bottom) {

    // ── 第一步:→ 沿顶部行从左到右 ──────────────────────────
    // 固定行为 top,列从 left 走到 right
    for (let i = left; i <= right; i++) {
      result.push(matrix[top][i]);
    }
    // 顶部行已全部收集,将上边界下移,把这一行"划出"可访问区域
    top++;

    // ── 第二步:↓ 沿右侧列从上到下 ──────────────────────────
    // 注意:top 已经加过 1,所以从新的 top 开始,避免重复收集右上角
    for (let i = top; i <= bottom; i++) {
      result.push(matrix[i][right]);
    }
    // 右侧列已全部收集,将右边界左移
    right--;

    // ── 第三步:← 沿底部行从右到左 ──────────────────────────
    // 必须先确认 top <= bottom:
    // 如果矩阵只有一行(或剩余区域只有一行),第一步已经把它收集完了,
    // 此时 top > bottom,底部行根本不存在,跳过可避免重复采集。
    if (top <= bottom) {
      for (let i = right; i >= left; i--) {
        result.push(matrix[bottom][i]);
      }
      // 底部行已全部收集,将下边界上移
      bottom--;
    }

    // ── 第四步:↑ 沿左侧列从下到上 ──────────────────────────
    // 同理,必须先确认 left <= right:
    // 如果矩阵只有一列(或剩余区域只有一列),第二步已经把它收集完了,
    // 此时 left > right,左侧列不存在,跳过可避免重复采集。
    if (left <= right) {
      for (let i = bottom; i >= top; i--) {
        result.push(matrix[i][left]);
      }
      // 左侧列已全部收集,将左边界右移
      left++;
    }
  }

  return result;
}

复杂度分析

  • 时间复杂度O(m × n),矩阵中每个元素恰好被访问一次。
  • 空间复杂度O(1),不计返回数组,只使用了四个边界变量。

示例演示

以如下 3 × 4 矩阵为例:

 1  2  3  4
 5  6  7  8
 9 10 11 12

第一轮:

阶段操作收集元素
第一阶段遍历 top 行(行 0,列 0→3)1, 2, 3, 4
第二阶段遍历 right 列(列 3,行 1→2)8, 12
第三阶段遍历 bottom 行(行 2,列 2→0)11, 10, 9
第四阶段遍历 left 列(列 0,行 1→1)5

收缩后边界变为:top=1, bottom=1, left=1, right=2

第二轮:

阶段操作收集元素
第一阶段遍历 top 行(行 1,列 1→2)6, 7
第二阶段遍历 right 列(列 2,行 2→1,已无元素)
第三阶段top(2) > bottom(1),跳过
第四阶段left(1) > right(1) 不成立,跳过

最终结果:[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]