title: Spiral Matrix
slug: spiral-matrix
difficulty: medium
leetcode_id: 54
leetcode_url: https://leetcode.com/problems/spiral-matrix/
categories:
  - arrays
patterns:
  - matrix-traversal

description: |
  Given an `m x n` `matrix`, return *all elements of the matrix in spiral order*.

  A **spiral order** traversal starts from the top-left corner and moves right, then down, then left, then up, and repeats this pattern, moving inward with each cycle until all elements are visited.

constraints: |
  - `m == matrix.length`
  - `n == matrix[i].length`
  - `1 <= m, n <= 10`
  - `-100 <= matrix[i][j] <= 100`

examples:
  - input: "matrix = [[1,2,3],[4,5,6],[7,8,9]]"
    output: "[1,2,3,6,9,8,7,4,5]"
    explanation: "Start at top-left, go right (1,2,3), down (6,9), left (8,7), up (4), then right to centre (5)."
  - input: "matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]"
    output: "[1,2,3,4,8,12,11,10,9,5,6,7]"
    explanation: "Traverse the outer ring first, then the inner row."

explanation:
  intuition: |
    Imagine peeling an onion layer by layer. The spiral traversal works the same way — we traverse the **outermost ring** of the matrix first, then move inward to the next ring, and repeat until all elements are visited.

    For each ring, we follow four directions in order:
    1. **Right**: across the top row
    2. **Down**: along the right column
    3. **Left**: across the bottom row (in reverse)
    4. **Up**: along the left column (in reverse)

    Think of it like this: we maintain four boundaries — `top`, `bottom`, `left`, and `right` — that define the current ring. After traversing each edge, we shrink the boundary inward. When the boundaries cross each other, we've visited everything.

    The key insight is that by tracking these four boundaries, we always know exactly where to go next without needing to mark cells as visited.

  approach: |
    We solve this using **Boundary Simulation**:

    **Step 1: Initialise the boundaries**

    - `top`: Set to `0` — the first row to process
    - `bottom`: Set to `m - 1` — the last row to process
    - `left`: Set to `0` — the first column to process
    - `right`: Set to `n - 1` — the last column to process
    - `result`: Empty list to store the spiral order

    &nbsp;

    **Step 2: Traverse while boundaries are valid**

    - Continue while `top <= bottom` and `left <= right`
    - For each iteration, traverse one complete ring:

    &nbsp;

    **Step 3: Traverse the four edges of the current ring**

    - **Right**: Traverse from `left` to `right` along row `top`, then increment `top`
    - **Down**: Traverse from `top` to `bottom` along column `right`, then decrement `right`
    - **Left**: If `top <= bottom`, traverse from `right` to `left` along row `bottom`, then decrement `bottom`
    - **Up**: If `left <= right`, traverse from `bottom` to `top` along column `left`, then increment `left`

    &nbsp;

    **Step 4: Return the result**

    - Return the `result` list containing all elements in spiral order

    &nbsp;

    The boundary checks before the left and up traversals handle matrices that aren't square — when one dimension runs out before the other.

  common_pitfalls:
    - title: Forgetting Boundary Checks on Inner Traversals
      description: |
        After traversing right and down, you must check if the boundaries are still valid before traversing left and up.

        For example, in a single-row matrix `[[1,2,3]]`, after going right and down (which adds nothing), the top boundary moves past bottom. Without checking `top <= bottom` before the left traversal, you'd incorrectly traverse again.
      wrong_approach: "Always traverse all four directions"
      correct_approach: "Check top <= bottom before left, and left <= right before up"

    - title: Off-by-One Errors in Range
      description: |
        Python's `range()` is exclusive on the end. For leftward and upward traversals, you need `range(right, left - 1, -1)` and `range(bottom, top - 1, -1)` to include the boundary positions.

        A common mistake is using `range(right, left, -1)` which misses the leftmost column.
      wrong_approach: "range(right, left, -1) — misses left boundary"
      correct_approach: "range(right, left - 1, -1) — includes left boundary"

    - title: Handling Single Row or Column
      description: |
        When the matrix is a single row or single column, the spiral simplifies to a straight line. Your algorithm should handle this naturally without special cases if the boundary checks are correct.

        Test with `[[1,2,3,4]]` (single row) and `[[1],[2],[3]]` (single column) to verify.
      wrong_approach: "Assuming matrix is always multi-row and multi-column"
      correct_approach: "Boundary checks handle edge cases automatically"

  key_takeaways:
    - "**Boundary tracking**: Use four variables (`top`, `bottom`, `left`, `right`) to define the current ring being traversed"
    - "**Shrink inward**: After each edge traversal, move the corresponding boundary inward"
    - "**Conditional checks**: Always check if boundaries are still valid before the left and up traversals"
    - "**Pattern recognition**: This technique applies to many matrix problems — spiral printing, rotating matrices, and layer-by-layer processing"

  time_complexity: "O(m × n). We visit each cell exactly once."
  space_complexity: "O(1). We only use a few boundary variables. The output list doesn't count toward auxiliary space."

solutions:
  - approach_name: Boundary Simulation
    is_optimal: true
    code: |
      def spiral_order(matrix: list[list[int]]) -> list[int]:
          if not matrix or not matrix[0]:
              return []

          result = []
          top, bottom = 0, len(matrix) - 1
          left, right = 0, len(matrix[0]) - 1

          while top <= bottom and left <= right:
              # Traverse right along the top row
              for col in range(left, right + 1):
                  result.append(matrix[top][col])
              top += 1

              # Traverse down along the right column
              for row in range(top, bottom + 1):
                  result.append(matrix[row][right])
              right -= 1

              # Traverse left along the bottom row (if rows remain)
              if top <= bottom:
                  for col in range(right, left - 1, -1):
                      result.append(matrix[bottom][col])
                  bottom -= 1

              # Traverse up along the left column (if columns remain)
              if left <= right:
                  for row in range(bottom, top - 1, -1):
                      result.append(matrix[row][left])
                  left += 1

          return result
    explanation: |
      **Time Complexity:** O(m × n) — Each cell is visited exactly once.

      **Space Complexity:** O(1) — Only boundary variables are used.

      We simulate the spiral by maintaining four boundaries. For each ring, we traverse right, down, left, and up, shrinking the boundaries after each direction. The key is checking if boundaries are still valid before the reverse traversals.

  - approach_name: Direction Vectors
    is_optimal: false
    code: |
      def spiral_order(matrix: list[list[int]]) -> list[int]:
          if not matrix or not matrix[0]:
              return []

          m, n = len(matrix), len(matrix[0])
          result = []
          visited = [[False] * n for _ in range(m)]

          # Direction vectors: right, down, left, up
          dr = [0, 1, 0, -1]
          dc = [1, 0, -1, 0]
          direction = 0  # Start going right

          row, col = 0, 0
          for _ in range(m * n):
              result.append(matrix[row][col])
              visited[row][col] = True

              # Calculate next position
              next_row = row + dr[direction]
              next_col = col + dc[direction]

              # Check if we need to turn
              if (next_row < 0 or next_row >= m or
                  next_col < 0 or next_col >= n or
                  visited[next_row][next_col]):
                  # Turn clockwise
                  direction = (direction + 1) % 4
                  next_row = row + dr[direction]
                  next_col = col + dc[direction]

              row, col = next_row, next_col

          return result
    explanation: |
      **Time Complexity:** O(m × n) — Each cell is visited once.

      **Space Complexity:** O(m × n) — The visited matrix.

      This approach uses direction vectors to simulate movement. We track visited cells and change direction (clockwise) when we hit a boundary or visited cell. While correct, it uses more space than the boundary approach.
