HARD Lab · Matrix · 60

54. Spiral Matrix

Traverse an m × n matrix in spiral order by maintaining four shrinking boundaries — top, bottom, left, right — and peeling one ring per while-loop iteration.

MediumMatrixBoundary SimulationTypeScript

PROBLEM What we're solving

Given a 2D matrix, return all elements in spiral order — clockwise from the top-left corner. For the matrix [[1,2,3],[4,5,6],[7,8,9]] the answer is [1,2,3,6,9,8,7,4,5]: right across row 0, down column 2, left across row 2, up column 0, then the lone center cell 5. This pattern appears in image rotation, memory layout, and printer rasterization.

KEY IDEA Four shrinking boundaries peel each ring

Insight → the spiral is just a loop that repeatedly peels the outermost ring of a shrinking rectangle. Keep four integer pointers — top, bottom, left, right — and after each side is traversed, advance the corresponding boundary inward. No direction array, no visited set; the boundaries do all the bookkeeping.

RECIPE Peel each ring in four passes

  • 0 · Initialize. top=0, bottom=rows-1, left=0, right=cols-1. Loop while top <= bottom && left <= right.
  • 1 · → Top row. Walk c = left…right along matrix[top][c]. Then top++ to shrink the top boundary.
  • 2 · ↓ Right column. Walk r = top…bottom along matrix[r][right]. Then right--.
  • 3 · ← Bottom row (guard). Only if top <= bottom (prevents re-visiting a single middle row). Walk right-to-left, then bottom--.
  • 4 · ↑ Left column (guard). Only if left <= right (prevents re-visiting a single middle column). Walk bottom-to-top, then left++.
Classic confusion → forgetting the two guards on steps 3 and 4. After a wide matrix exhausts to a single row (like a 1×3 strip), steps 1–2 consume it; without the top <= bottom check, step 3 would walk it backwards, duplicating values. Always guard the last two directions.

COST Complexity & alternatives

Direction-array + visited[][]
O(m·n)
Same time, but O(m·n) extra space for the visited grid.
Four-boundary simulation
O(m·n)
O(1) extra space — the four ints are all state you need.

Both approaches are O(m·n) time since every cell is visited exactly once. The direction-array approach is more general (works for Spiral Matrix II — generating the spiral) but allocates a full visited matrix. The boundary approach is the space-optimal choice for read-only traversal.

Pattern transfer → boundary simulation is reused verbatim in Spiral Matrix II (fill instead of read), Rotate Image (boundary-based ring rotation), and Set Matrix Zeroes (boundary markers). Any problem that processes a 2D grid ring-by-ring is this pattern.

RUN IT Peel the spiral ring by ring

step 0 / 10
STARTStart spiral: four boundaries shrink inward as we peel each ring. top=0 bottom=2 left=0 right=2.
1
2
3
4
5
6
7
8
9
Boundaries
top=0 bottom=2
left=0 right=2
Result so far
[]
current cellalready collecteddone / all collected
slowfast

TYPESCRIPT The solution, annotated

spiralOrder.ts
function spiralOrder(matrix: number[][]): number[] {
  const result: number[] = [];
  let top = 0, bottom = matrix.length - 1;
  let left = 0, right = matrix[0].length - 1;

  while (top <= bottom && left <= right) {
    // → traverse top row left to right
    for (let c = left; c <= right; c++) result.push(matrix[top][c]);
    top++;

    // ↓ traverse right column top to bottom
    for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
    right--;

    // ← traverse bottom row right to left (guard: top ≤ bottom)
    if (top <= bottom) {
      for (let c = right; c >= left; c--) result.push(matrix[bottom][c]);
      bottom--;
    }

    // ↑ traverse left column bottom to top (guard: left ≤ right)
    if (left <= right) {
      for (let r = bottom; r >= top; r--) result.push(matrix[r][left]);
      left++;
    }
  }

  return result;
}

Reading it block by block

Lines 3–4 — initialize boundaries. top and bottom are the inclusive row bounds; left and right are the inclusive column bounds. The while condition keeps looping as long as there is at least one row and one column left to process.
Lines 8–9 — top row, left → right. Walk every column from left to right along matrix[top][c]. After the pass, top++ retires that row: it will never be touched again.
Lines 12–13 — right column, top → bottom. The updated top (post-increment) means we start below the row we just finished. After the pass, right-- retires the right column.
Lines 16–19 — bottom row, right → left (guarded). The top <= bottomguard catches the case where the remaining "ring" is a single row — it was already consumed in step 1, so we must not walk it again. After the pass, bottom--.
Lines 22–25 — left column, bottom → top (guarded). The left <= right guard catches the single-column degenerate case. After the pass, left++. The while condition re-evaluates: if both bounds have crossed, we exit with every cell collected.
Complexity → O(m·n) time — every cell is pushed exactly once. O(1) extra space — only four integer boundary pointers; the output array is required by the problem and not counted.

INTERVIEWFollow-ups they'll ask

  • "Spiral Matrix II?" Instead of reading, write the values 1…n² into a freshly allocated matrix using the same boundary loop — same structure, reversed data flow.
  • "Can you do it recursively?" Peel the first ring, then recursively call on the sub-matrix. Elegant but uses O(min(m,n)) call-stack depth.
  • "What if the matrix is 1×n or n×1?" The guards on steps 3 and 4 handle these automatically — no special case needed. Trace a 1×3 to convince yourself.
  • "Rotate Image?" That problem does a boundary-based ring rotation in-place: the same four-pointer approach but swapping four symmetric cells per step rather than collecting them.
  • "Return the actual spiral path, not just values?" Change result.push(matrix[r][c]) to result.push([r, c]) — zero algorithmic change.

MNEMONIC The one-liner

"Four walls close in: right, down, left, up — guard the last two or you double-dip."

TRIGGERS When you see ___ → reach for ___

"return elements in spiral order"four-boundary simulation
process a 2D grid ring by ringtop/bottom/left/right pointers
need O(1) space matrix traversalboundary pointers (no visited array)
"spiral matrix II / rotate image"same boundary loop, write instead of read

SKELETON The reusable shape

skeleton.ts
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
const result: number[] = [];

while (top <= bottom && left <= right) {
  for (let c = left; c <= right; c++) result.push(matrix[top][c]);
  top++;
  for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
  right--;
  if (top <= bottom) { for (let c = right; c >= left; c--) result.push(matrix[bottom][c]); bottom--; }
  if (left <= right) { for (let r = bottom; r >= top; r--) result.push(matrix[r][left]); left++; }
}
return result;

FLASHCARDS Tap to flip

What do the four boundaries represent?
Inclusive row/column limits of the unvisited rectangle: top, bottom, left, right.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the spiral order of [[1,2,3],[4,5,6],[7,8,9]]?
QUESTION 02
Time complexity of the boundary-simulation approach?
QUESTION 03
Why must the bottom-row pass be guarded with if (top <= bottom)?
QUESTION 04
Extra space used by the four-boundary approach (excluding the output array)?
QUESTION 05
For a 1×4 matrix [[1,2,3,4]], which passes actually execute?
QUESTION 06
What change makes this solution fill a spiral (Spiral Matrix II) instead of reading one?
QUESTION 07
After processing the top row and right column in the first ring of a 3×3 matrix, how many boundary variables have changed?