/Interview Study Guide/Algorithms & data structures
#62

Spiral Matrix

medium
arraymatrixsimulation

Given an m x n matrix, return all of its elements in spiral order: start at the top-left, move right across the top row, down the right column, left across the bottom row, up the left column, and continue spiralling inward until every element is visited.

Example

Input: matrix =
123
456
789
Output: [1,2,3,6,9,8,7,4,5]

Right across the top, down the right edge, left along the bottom, up the left edge, then the center.

Constraints

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

Intuition

A direct way to trace the spiral is to walk the grid like a robot: keep moving in the current direction, and only turn — clockwise — the moment the next cell would leave the grid or has already been visited, tracking every visited cell in its own boolean grid.

function spiralOrder(matrix) {
  const rows = matrix.length;
  const cols = matrix[0].length;
  const result = [];
  // Remember every cell already emitted — "already visited" is how we know when to turn.
  const visited = Array.from({ length: rows }, () => new Array(cols).fill(false));
  // Clockwise order: right, down, left, up.
  const directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
  let dir = 0;
  let row = 0;
  let col = 0;
  for (let step = 0; step < rows * cols; step++) {
    result.push(matrix[row][col]);
    visited[row][col] = true;
    const [dr, dc] = directions[dir];
    const nextRow = row + dr;
    const nextCol = col + dc;
    // Turn clockwise the instant the next cell falls off the grid or was already emitted.
    const blocked = nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || visited[nextRow][nextCol];
    if (blocked) dir = (dir + 1) % 4;
    row += directions[dir][0];
    col += directions[dir][1];
  }
  return result;
}
Brute force — simulate the walk, turning on a wall or a revisited cell: O(m·n) time, O(m·n) extra space.

This still visits every cell exactly once — O(m·n) time either way — but it pays for an O(m·n) visited grid just to answer one question at each step: have I been here before? Can we do better?

The key observation: the spiral doesn't need to ask that per cell, because it only ever turns at the same four moments each lap — right after finishing the top row, the right column, the bottom row, and the left column. So instead of remembering which cells are done, track which rows and columns are done, with four numbers: top, bottom, left, right. Walk one full edge along each bound, shrink that bound inward by one, and repeat until the bounds cross. It's the same "has this region already been consumed" bookkeeping that shows up across Matrices & grids ring-walk problems (matrix rotation, border traversal) — just four numbers standing in for a full visited grid.

The one wrinkle is a matrix that isn't square: after the top-row and right-column passes, a single remaining row or column would otherwise get walked twice — once as the "bottom row", again as the "left column" — unless those two passes are guarded with if (top <= bottom) and if (left <= right) and skipped once nothing is left along that edge.

Walking it through:

matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]

0123
01234
15678
29101112
top row → right: 1, 2, 3, 4; top++ (top = 1)

Walk the current top row left to right, then move the top bound down past it.

0123
01234
15678
29101112
right col → down: 8, 12; right-- (right = 2)

Walk the right column top to bottom — row 0's corner was already taken — then pull the right bound in.

0123
01234
15678
29101112
top (1) ≤ bottom (2) → bottom row → left: 11, 10, 9; bottom-- (bottom = 1)

The guard passes: top hasn't crossed bottom yet, so the bottom row still has cells the first pass never touched.

0123
01234
15678
29101112
left (0) ≤ right (2) → left col → up: 5; left++ (left = 1)

Same guard on the other axis: left hasn't crossed right, so there's one cell left above the corner already taken.

0123
01234
15678
29101112
top row → right: 6, 7; top++ (top = 2)

The bounds now describe a single remaining row — a 1×2 sliver. It's still a valid ring, so the same top-row pass walks it.

0123
01234
15678
29101112
top (2) > bottom (1) → bottom row skipped; loop ends

The right-column and left-column passes run but find nothing left in their range, and the `if (top <= bottom)` guard now evaluates false — exactly the check that stops the bottom row from being walked a second time on a matrix that isn't square. Result: 1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7.

Optimization

Shrinking boundaries

Keep four boundary indices — top, bottom, left, right — and peel one edge at a time: the top row (left→right), the right column (top→bottom), the bottom row (right→left), the left column (bottom→top). After each edge, move that boundary inward. The two inner walks are guarded so a single remaining row or column isn't visited twice.

O(m·n) time, O(1) extra space beyond the output.

function spiralOrder(matrix) {
  const out = [];
  // Four bounds mark the current outer ring; each shrinks inward once its edge is walked.
  let top = 0;
  let bottom = matrix.length - 1;
  let left = 0;
  let right = matrix[0].length - 1;
  while (top <= bottom && left <= right) {
    // Top row, left to right.
    for (let j = left; j <= right; j++) out.push(matrix[top][j]);
    top++;
    // Right column, top to bottom (row "top" is now one past the row just consumed).
    for (let i = top; i <= bottom; i++) out.push(matrix[i][right]);
    right--;
    // Guard: a single remaining row would already be exhausted by the two passes above.
    if (top <= bottom) {
      // Bottom row, right to left.
      for (let j = right; j >= left; j--) out.push(matrix[bottom][j]);
      bottom--;
    }
    // Guard: a single remaining column would already be exhausted by the passes above.
    if (left <= right) {
      // Left column, bottom to top.
      for (let i = bottom; i >= top; i--) out.push(matrix[i][left]);
      left++;
    }
  }
  return out;
}

Complexity analysis

Time complexity: O(m·n). Here's why:

  • Each of the four edge passes (top row, right column, bottom row, left column) advances a bound that only ever shrinks, so across the whole run each bound moves inward at most m or n times total.
  • Together the four passes partition the grid into concentric rings, and every cell is pushed to the output exactly once as its ring is walked.

So the total work is proportional to the cell count: O(m·n), where m and n are the matrix's dimensions. We can't do better — any correct solution has to look at every cell at least once.

Space complexity: O(1) extra, not counting the output. Here's why:

  • The brute-force baseline keeps a visited boolean grid with one entry per cell — O(m·n) extra space.
  • The stored solution drops that grid entirely: it tracks only the four bound variables top, bottom, left, right — O(1) extra space, regardless of the matrix's size.

So the optimization trades the O(m·n) visited grid for O(1) extra space. The only space that scales with the input is the out array holding the m × n result values, which every correct solution has to produce.

Test cases

Beyond the example above, these are worth thinking through before you submit.

InputExpected outputDescription
matrix =
42
[42]1×1 matrix — the top-row pass emits the only cell, then both bounds immediately cross.
matrix =
102030
[10,20,30]A single row: only the top-row pass ever fires; the guarded bottom-row pass never runs because bottom < top right after it.
matrix =
1
2
3
4
[1,2,3,4]A single column: the top-row pass takes one cell, then the (unguarded) right-column pass walks the rest top to bottom.
matrix =
55
55
[5,5,5,5]Every value identical — the walk is purely positional, so four indistinguishable values still come out in the right order.
matrix =
121
212
[1,2,1,2,1,2]Repeated values scattered across the grid — the algorithm tracks indices, not values, so duplicates don't confuse the walk.

Try it yourself

Write your solution against the real judge before checking the reference.

Open in editor