Skip to content
intermediate

Spiral Matrix

A spiral traversal can look correct on a square matrix and still fail immediately on a single row or column. The reliable model is a shrinking rectangle:…

Published 2026-09-07Updated 2026-09-1210 min read
Vibrant close-up of network cable connectors with colorful lighting.
Vibrant close-up of network cable connectors with colorful lighting. Photo by Nic Wood on Pexels.
Problem

Spiral Matrix

Difficulty: MediumAcceptance rate: 57.7%

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

ArrayMatrixSimulation

Constraints

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

Important details

  • The input is an m x n rectangular matrix, so every row has n elements.
  • Return each matrix element once in the required spiral traversal order.

A spiral traversal can look correct on a square matrix and still fail immediately on a single row or column. The reliable model is a shrinking rectangle: track its four inclusive boundaries, consume one side at a time, and guard any side that may have collapsed.

Read the contract and spot the pattern

The task is narrow:

  • Input: an m × n rectangular matrix.
  • Output: every element exactly once.
  • Order: clockwise spiral order, starting at the top-left.
  • Direction cycle: right, down, left, up, repeating toward the center.

For example:

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

The spiral order is:

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

The useful pattern signal is structural: after the outer ring is consumed, the unvisited cells still form a rectangle. That rectangle gets smaller after each pass.

This is a matrix boundary traversal, not graph search. We do not need a visited matrix because the boundaries themselves describe which cells remain unvisited.

The required result list already takes O(mn) space. The goal is to use only O(1) additional traversal state.

Why naive simulation repeats cells

The most direct approach is to simulate movement:

  1. Start at the top-left.
  2. Move in the current direction.
  3. Turn clockwise when the next cell is outside the matrix or already visited.
  4. Record each cell in a result list.
  5. Track visited cells in a boolean matrix.

That approach is valid and often easy to visualize. It touches each cell once, but the visited matrix costs O(mn) auxiliary space.

There is a more dangerous version of the same idea: write four loops for the top row, right column, bottom row, and left column, but omit the conditions that check whether those sides still exist.

Consider a one-row matrix:

1 2 3 4

The top-row pass correctly appends all four values. If the code then blindly runs the bottom-row pass, it walks across the same row in reverse and appends:

4 3 2 1

A one-column matrix creates the same problem vertically. The final “ring” may be only one row, one column, or one cell. The traversal must recognize that collapse instead of treating every pass as a distinct side.

The interview target is therefore:

Keep the clear directional sequence, but encode already-consumed territory with four boundary indices instead of a visited grid.

Track the remaining rectangle

A three-by-four matrix shown across successive stages: the outer top row, right column, bottom row, and left column are highlighted clockwise, leaving the inner two-cell rectangle; labels show top, bottom, left, and right boundaries moving inward.
Each clockwise pass consumes one boundary and leaves a smaller rectangle; the conditional bottom and left passes handle collapsed rows and columns safely.

Use four inclusive boundaries:

  • top: first unvisited row
  • bottom: last unvisited row
  • left: first unvisited column
  • right: last unvisited column

Initially, the entire matrix is unvisited:

top = 0
bottom = m - 1
left = 0
right = n - 1

The key invariant is:

Before every iteration, all unvisited cells are inside top <= bottom and left <= right. Every cell outside those boundaries has already been appended.

Each iteration consumes the current rectangle in clockwise order.

1. Traverse the top row

Read columns from left through right:

matrix[top][left], matrix[top][left + 1], ..., matrix[top][right]

Then that row is finished:

top += 1

2. Traverse the right column

Read rows from the new top through bottom:

matrix[top][right], matrix[top + 1][right], ..., matrix[bottom][right]

Then that column is finished:

right -= 1

Starting the right-column pass at the updated top prevents the top-right corner from being emitted twice.

3. Traverse the bottom row if it still exists

The top-row pass may have consumed the only remaining row. Only traverse the bottom row when:

top <= bottom

Read from right down to left, then shrink:

bottom -= 1

This guard matters because top > bottom means the remaining height is zero.

4. Traverse the left column if it still exists

The right-column pass may have consumed the only remaining column. Only traverse the left column when:

left <= right

Read from bottom up to top, then shrink:

left += 1

The order of the updates matters. The bottom-row guard comes after top has moved inward. The left-column guard comes after right has moved inward and after the bottom row may have been consumed.

That is the whole algorithm: walk four sides, move four boundaries, repeat.

Implement the Spiral Matrix solution in Python

def spiral_order(matrix: list[list[int]]) -> list[int]:
    if not matrix or not matrix[0]:
        return []

    m = len(matrix)
    n = len(matrix[0])

    result = []

    top = 0
    bottom = m - 1
    left = 0
    right = n - 1

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

        # Right column: top to bottom.
        for row in range(top, bottom + 1):
            result.append(matrix[row][right])
        right -= 1

        # Bottom row: right to left, only if a row remains.
        if top <= bottom:
            for col in range(right, left - 1, -1):
                result.append(matrix[bottom][col])
            bottom -= 1

        # Left column: bottom to top, only if a column remains.
        if left <= right:
            for row in range(bottom, top - 1, -1):
                result.append(matrix[row][left])
            left += 1

    return result

There are two details I check immediately when writing this in an interview:

  1. n must come from len(matrix[0]), not len(matrix). Rectangular matrices can have different row and column counts.
  2. The bottom-row and left-column passes are conditional. Those guards are the difference between a boundary traversal and a duplicate-producing loop.

The outer condition:

while top <= bottom and left <= right:

means a valid rectangle remains. Once either dimension collapses, traversal stops.

Prove coverage and no duplication

A solution that matches one square example is not yet a solution. The boundary invariant gives a short correctness argument.

Coverage

At the start of each iteration, every unvisited cell lies inside the current rectangle.

  • The top-row pass consumes the rectangle's first row.
  • The right-column pass consumes its right edge, excluding the already-consumed top-right corner.
  • The bottom-row pass consumes its last row if that row still exists.
  • The left-column pass consumes its left edge if that column still exists.

After each pass, the corresponding boundary moves inward. Therefore, the next iteration considers only cells that have not been consumed by an earlier pass.

Because the rectangle shrinks inward, the process eventually reaches every matrix coordinate.

No duplication

The corners are where careless implementations fail.

  • The right-column loop starts at top, after the top row has been removed. It does not revisit the top-right corner.
  • The bottom-row loop runs only if top <= bottom. If the top pass consumed the last remaining row, the bottom row is skipped.
  • The left-column loop runs only if left <= right. If the right-column pass consumed the last remaining column, the left column is skipped.
  • Its starting row is the updated bottom, and its ending row is the updated top, so it does not revisit cells already consumed on the bottom row.

The stopping condition proves termination: every complete side moves a boundary inward, and no boundary can move outward.

A useful implementation check is:

len(spiral_order(matrix)) == len(matrix) * len(matrix[0])

That checks coverage count, though not order. To verify order, inspect a rectangular case where row and column values make accidental reversals obvious.

Dry-run rectangular and singleton matrices

A 3 × 4 matrix

Start with:

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

Initial state:

top = 0, bottom = 2, left = 0, right = 3
PassBoundaries before passAppended valuesUpdated boundary
Top rowtop=0, left=0..31, 2, 3, 4top=1
Right columnright=3, row=1..28, 12right=2
Bottom rowbottom=2, col=2..011, 10, 9bottom=1
Left columnleft=0, row=1..15left=1

The remaining rectangle is:

6 7

Its state is:

top = 1, bottom = 1, left = 1, right = 2

The next iteration appends:

6, 7

The final result is:

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

Notice the state, not the picture. The picture helps at first, but the boundaries are what make the code reliable.

A 1 × 4 matrix

1 2 3 4

Initial state:

top = 0, bottom = 0, left = 0, right = 3

The top-row pass appends all four values and changes top to 1.

Now:

top = 1, bottom = 0

No row remains, so the bottom-row guard fails. The left-column guard also fails because right has already moved to 2, but the row range is empty.

Result:

[1, 2, 3, 4]

A 4 × 1 matrix

1
2
3
4

The top-row pass appends 1. The right-column pass then appends 2, 3, 4 from top to bottom.

After that:

right = -1

The left-column guard fails because left <= right is false. The traversal does not walk upward through the same column again.

Result:

[1, 2, 3, 4]

A 3 × 3 matrix

1 2 3
4 5 6
7 8 9

The outer ring produces:

[1, 2, 3, 6, 9, 8, 7, 4]

The remaining rectangle is the single cell 5. The loop processes that one-cell rectangle once:

[1, 2, 3, 6, 9, 8, 7, 4, 5]

The same guards that handle one row or one column also protect the center of an odd-sized matrix.

Complexity and interview checks

Let m be the number of rows and n the number of columns.

Time

The algorithm is O(mn).

Every matrix element is appended exactly once. The loops are split into four directions, but together they account for the full m × n matrix. Boundary comparisons add constant work per layer and do not change the total bound.

Space

The traversal uses O(1) auxiliary space for:

top, bottom, left, right

The output list requires O(mn) space because the problem explicitly asks us to return every element. When reporting space complexity, distinguish those two facts:

  • Auxiliary traversal space: O(1)
  • Required output space: O(mn)

Common bugs

Check these before submitting:

  • Using the row count for both dimensions.
    Use m = len(matrix) and n = len(matrix[0]).

  • Mixing inclusive and exclusive boundaries.
    This implementation uses inclusive top, bottom, left, and right everywhere.

  • Forgetting + 1 in an increasing range.
    range(left, right + 1) must include right.

  • Forgetting the lower endpoint in a reverse range.
    range(right, left - 1, -1) must include left.

  • Updating the wrong boundary.
    Top row consumes top; right column consumes right; bottom row consumes bottom; left column consumes left.

  • Omitting one of the two guards.
    The bottom row and left column may no longer be distinct sides.

  • Testing only square matrices.
    A square example can hide both dimension mistakes and collapsed-boundary mistakes.

My interview test set is small and deliberate: one rectangular matrix, one single-row matrix, one single-column matrix, and one odd-dimension matrix with a center cell. Read the output. Trace the four boundaries. Fix the assumption exposed by the first failure.

The transferable rule is simple:

When a process peels a rectangular region and completed edges must never be revisited, represent the unprocessed region explicitly. State the boundary invariant first, then guard every side that may have collapsed.

Implement the four-boundary state from that rule—not from memorized loop syntax.

References

  1. leetcode/solution/0000-0099/0054.Spiral Matrix ...github.com
  2. Print a given matrix in spiral form - GeeksforGeekswww.geeksforgeeks.org
8sources checked
8source domains
5searches run

Research updated Sep 7, 2026

Related sites

Strengthen the language foundations behind the solution

Use LearnPyFast and LearnJSFast when you want to reinforce the language mechanics that support interview implementations.

Python tutorialstutorial

LearnPyFast

Beginner-friendly Python tutorials, examples, and learning paths for practical programming foundations.

PythonProgrammingBeginners
Visit LearnPyFast
JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast

Keep grinding

Related coding interview problems

Continue with nearby problems that reuse the same data-structure, invariant, or optimization pattern.

From above of green leaves on thin branches of plant growing in botanical garden
intermediate
10 min read

Rotate Image

The hard part of rotating a matrix is not visualizing the turn. It is moving every value without destroying one that has not moved yet.

View solution
Close-up of a tablet displaying analytics charts on a wooden office desk, alongside a smartphone and coffee cup.
intermediate
12 min read

Set Matrix Zeroes

The dangerous part is not writing zeroes. It is remembering which zeroes were causes and which zeroes were created by your own writes.

View solution
Detailed view of a circular saw blade in an industrial workshop, showcasing precision and craftsmanship.
intermediate
8 min read

Spiral Matrix II

The key distinction is simple: Spiral Matrix reads values from an existing grid; Spiral Matrix II constructs the grid while the spiral advances. The…

View solution