Algorithm

Spiral Matrix

Arrays & Strings Pattern

Spiral Matrix

Given an m x n matrix, return all elements of the matrix in spiral order.

CONSTRAINTS
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100
EXAMPLE 1
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [1,2,3,6,9,8,7,4,5]
Full perimeter clockwise, then the single center cell.
EXAMPLE 2
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]
Rectangular 3×4: after the outer loop, the surviving region is the single middle row [6, 7], read left to right.
EXAMPLE 3
Input: matrix = [[7],[9],[6]]
Output: [7,9,6]
A single column: the top edge takes 7, the right edge walks down through 9 and 6. The guards prevent walking back up.
Can the matrix be rectangular, not square?
Yes — m and n can differ, and that is where solutions break. The inner layers can degenerate into a single row or column, so the completion check after every edge (not just per loop) is what prevents duplicate visits.
Is a single row or single column valid input?
Yes. A 1×n matrix is just its row left to right; an m×1 matrix is its column top to bottom. Both fall out of the fence logic with the guards in place.
Should I return the elements or print them?
Return them as a flat list of m×n values in visiting order.

Spiral order reads the grid the way you would peel it: start at the top-left, walk right along the top edge, down the right edge, left along the bottom edge, up the left edge — then repeat the same clockwise loop one layer further in, until every cell is collected. Two things make it harder than it looks: knowing exactly when to turn, and never visiting a cell twice — especially in rectangular (non-square) grids, where the last surviving layer may be a single row or single column.

Simulate it: walk until blocked, then turn

The most direct translation: keep a current direction (right, down, left, up as coordinate deltas), step forward while the next cell is inside the grid and unvisited, otherwise turn clockwise. A set remembers visited cells.

python
res = []
visited = set()
r, c = 0, 0
dr, dc = [0, 1, 0, -1], [1, 0, -1, 0]   # right, down, left, up
di = 0                                    # index of the current direction
for _ in range(rows * cols):
    res.append(matrix[r][c])
    visited.add((r, c))
    nr, nc = r + dr[di], c + dc[di]
    if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
        r, c = nr, nc
    else:
        di = (di + 1) % 4                 # blocked: turn clockwise once
        r, c = r + dr[di], c + dc[di]

This works, and the turning rule is honest: in a spiral you turn exactly when the cell ahead is either off the grid or already taken. The cost of knowing "already taken" is the visited set — O(M×N) extra memory on top of the walk.

The observation that deletes the visited set

Watch what each straight run consumes: the first run eats the entire top row; the next eats the entire remaining right column; then the remaining bottom row; then the remaining left column. After every run, one full row or column is permanently spent. Which means the unvisited region is always a clean rectangle — and a rectangle is fully described by four numbers: top, bottom, left, right. Instead of remembering M×N individual cells, maintain the invariant "everything inside the four fences is unvisited": walk an edge of the rectangle, then pull that fence inward by one. Turning happens exactly at the fences; revisiting is impossible because visited territory is, by construction, outside them.

python
res = []
top, bottom = 0, rows - 1
left, right = 0, cols - 1
while len(res) < rows * cols:
    for j in range(left, right + 1): res.append(matrix[top][j])
    top += 1                                  # top row spent
    if len(res) == rows * cols: break

    for i in range(top, bottom + 1): res.append(matrix[i][right])
    right -= 1                                # right column spent
    if len(res) == rows * cols: break

    for j in range(right, left - 1, -1): res.append(matrix[bottom][j])
    bottom -= 1                               # bottom row spent
    if len(res) == rows * cols: break

    for i in range(bottom, top - 1, -1): res.append(matrix[i][left])
    left += 1                                 # left column spent
return res
Crucial Notethe if len(res) == rows cols: break guard after every edge is not paranoia — it is where rectangular grids are won or lost. Suppose the surviving region is a single row: the top-edge walk consumes it entirely. Without the guard, the loop would continue to the bottom-edge walk — and bottom still points at that same* row, so it would be re-walked in reverse, emitting duplicates. Checking the count after each edge stops the machine the instant the last cell is collected.
Worked Example:[[1,2,3],[4,5,6],[7,8,9]]
- Top edge: collect 1, 2, 3. Fence: top moves to row 1.
- Right edge: collect 6, 9. Fence: right moves to column 1.
- Bottom edge (leftward): collect 8, 7. Fence: bottom moves to row 1.
- Left edge (upward): collect 4. Fence: left moves to column 1.
- Remaining rectangle is the single cell 5 — top edge of the inner layer collects it.
- Answer: [1, 2, 3, 6, 9, 8, 7, 4, 5].
The takeaway

O(M×N) time — optimal, since every element must be output — and O(1) extra space beyond the answer. The generalizable move: replace per-cell bookkeeping with a shape invariant. We never tracked which cells were visited; we maintained a guarantee ("the unvisited region is the rectangle inside the fences") that four integers could carry. Whenever per-item state feels unavoidable, ask whether the processed region has a simple describable shape — if it does, a handful of boundary variables can replace an entire data structure.

Interactive Strategy Visualization

Spiral Traversal

Boundary-Based Navigation
1
2
3
4
5
6
7
8
9

Starting at [0,0]. Moving Right along the Top Wall.

O(M×N) + Visited Set
O(M×N) + O(1) Shrinking Fences