Skip to content

Matrix and Grid Manipulation

Matrix questions are rarely about algorithms. They are about index discipline under pressure: can you rotate a grid without a scratch copy, walk a spiral without re-visiting a row, or mark rows and columns for deletion without a second array to mark them in?

Interviewers like them precisely because they are hard to bluff. There is no clever insight to recall — either your loop bounds are right or your output is scrambled. This page gives you the three reusable tricks that cover almost the whole family.

What you’ll learn

  • Transpose + reverse — rotation without allocating a second matrix.
  • The four-boundary template that makes spiral traversal off-by-one-proof.
  • Using the matrix’s own first row and column as O(1)O(1) marker storage.
  • Why “read and write in the same pass” is the recurring bug, and the two ways round it.
  • Three real LeetCode problems solved in the browser: 48, 54, 73.

The cue

Trick 1 — rotate by transpose, then reverse

Rotating 90° clockwise in place looks like it needs four-way cyclic swaps. It does not. It decomposes into two trivially correct passes:

rotate_in_place.py
def rotate(matrix):
    n = len(matrix)
 
    for i in range(n):                  # 1. transpose: swap across the diagonal
        for j in range(i + 1, n):       #    j starts at i+1, NOT 0
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
 
    for row in matrix:                  # 2. reverse each row
        row.reverse()
 
    return matrix
 
 
print(rotate([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
# [[7, 4, 1], [8, 5, 2], [9, 6, 3]]
rotate_in_place.py
def rotate(matrix):
    n = len(matrix)
 
    for i in range(n):                  # 1. transpose: swap across the diagonal
        for j in range(i + 1, n):       #    j starts at i+1, NOT 0
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
 
    for row in matrix:                  # 2. reverse each row
        row.reverse()
 
    return matrix
 
 
print(rotate([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
# [[7, 4, 1], [8, 5, 2], [9, 6, 3]]

The other three rotations are the same two moves in different combinations:

RotationRecipe
90° clockwisetranspose, then reverse each row
90° counter-clockwisetranspose, then reverse each column (i.e. matrix.reverse()matrix.reverse())
180°reverse each row and reverse the row order

Trick 2 — four boundaries for spiral traversal

Track toptop, bottombottom, leftleft, rightright. Walk one edge, then shrink that boundary inward. The whole difficulty is in two guards.

spiral_order.py
def spiral_order(matrix):
    if not matrix or not matrix[0]:
        return []
 
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    out = []
 
    while top <= bottom and left <= right:
        for c in range(left, right + 1):            # left -> right along the top
            out.append(matrix[top][c])
        top += 1
 
        for r in range(top, bottom + 1):            # top -> bottom down the right
            out.append(matrix[r][right])
        right -= 1
 
        if top <= bottom:                           # GUARD: row not already taken
            for c in range(right, left - 1, -1):    # right -> left along the bottom
                out.append(matrix[bottom][c])
            bottom -= 1
 
        if left <= right:                           # GUARD: column not already taken
            for r in range(bottom, top - 1, -1):    # bottom -> top up the left
                out.append(matrix[r][left])
            left += 1
 
    return out
 
 
print(spiral_order([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
# [1, 2, 3, 6, 9, 8, 7, 4, 5]
spiral_order.py
def spiral_order(matrix):
    if not matrix or not matrix[0]:
        return []
 
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    out = []
 
    while top <= bottom and left <= right:
        for c in range(left, right + 1):            # left -> right along the top
            out.append(matrix[top][c])
        top += 1
 
        for r in range(top, bottom + 1):            # top -> bottom down the right
            out.append(matrix[r][right])
        right -= 1
 
        if top <= bottom:                           # GUARD: row not already taken
            for c in range(right, left - 1, -1):    # right -> left along the bottom
                out.append(matrix[bottom][c])
            bottom -= 1
 
        if left <= right:                           # GUARD: column not already taken
            for r in range(bottom, top - 1, -1):    # bottom -> top up the left
                out.append(matrix[r][left])
            left += 1
 
    return out
 
 
print(spiral_order([[1, 2, 3], [4, 5, 6], [7, 8, 9]]))
# [1, 2, 3, 6, 9, 8, 7, 4, 5]

Trick 3 — the matrix as its own marker store

“Set entire row and column to zero wherever you find a zero” has an obvious O(m+n)O(m + n) solution: collect the row and column indices in two sets, then apply. The O(1)O(1)-space version stores those same flags in row 0 and column 0 of the matrix itself.

The catch: row 0 and column 0 are also real data, and they intersect at matrix[0][0]matrix[0][0], which cannot flag both. So handle them separately with two booleans, do the interior, then finish the borders last.

The variant map

VariantThe moveCanonical problem
Rotate 90°Transpose, then reverse rows48 Rotate Image
Transpose (non-square)Must allocate — dimensions change867 Transpose Matrix
Spiral readFour boundaries + two guards54 Spiral Matrix
Spiral writeSame walk, assigning 1..n²1..n² instead of reading59 Spiral Matrix II
Diagonal walkCells on a diagonal share r + cr + c; bucket by that sum498 Diagonal Traverse
Flag in placeUse row 0 / column 0 as marker storage73 Set Matrix Zeroes
Simultaneous updateEncode next state in spare bits, or use a second value289 Game of Life

Practice — real LeetCode problems

LC 48 — Rotate Image · Medium

Problem. Given an n x nn x n matrix, rotate it 90° clockwise in place. You must not allocate another matrix.

Constraints. n == len(matrix) == len(matrix[i])n == len(matrix) == len(matrix[i]), 1 <= n <= 201 <= n <= 20, -1000 <= matrix[i][j] <= 1000-1000 <= matrix[i][j] <= 1000.

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

Editorial — approach, complexity, follow-ups

A clockwise rotation sends (r, c)(r, c) to (c, n-1-r)(c, n-1-r). Doing that as a single four-way cyclic swap over each ring works, but the index expressions are easy to get wrong under pressure.

Decomposing is strictly better: transpose sends (r, c)(r, c) to (c, r)(c, r), and reversing each row then sends (c, r)(c, r) to (c, n-1-r)(c, n-1-r). Composition gives exactly the rotation, and each pass is individually obvious.

Time O(n2)O(n^2) — unavoidable, every element moves. Space O(1)O(1).

Follow-ups you should expect: “Counter-clockwise?” — transpose, then reverse the order of the rows (matrix.reverse()matrix.reverse()). “180°?” — reverse rows and reverse row order; no transpose. “Non-square?” — a rotation changes the dimensions, so in-place is impossible; allocate an n x mn x m result. “Do the ring-based four-way swap” — iterate for i in range(n // 2)for i in range(n // 2) over rings and for j in range(i, n - 1 - i)for j in range(i, n - 1 - i) within a ring, rotating four cells at a time; worth knowing but not worth preferring.

LC 54 — Spiral Matrix · Medium

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

Constraints. 1 <= m, n <= 101 <= m, n <= 10, -100 <= matrix[i][j] <= 100-100 <= matrix[i][j] <= 100.

Examples. [[1,2,3],[4,5,6],[7,8,9]][[1,2,3],[4,5,6],[7,8,9]] gives [1,2,3,6,9,8,7,4,5][1,2,3,6,9,8,7,4,5] · [[1,2,3,4],[5,6,7,8],[9,10,11,12]][[1,2,3,4],[5,6,7,8],[9,10,11,12]] gives [1,2,3,4,8,12,11,10,9,5,6,7][1,2,3,4,8,12,11,10,9,5,6,7]

Editorial — approach, complexity, follow-ups

Four boundaries, four walks, shrink after each. The outer condition top <= bottom and left <= righttop <= bottom and left <= right stops when the unvisited region is empty.

Time O(mn)O(m \cdot n). Space O(1)O(1) beyond the output.

The two guards handle the degenerate end state. After the top row and right column are consumed, the remaining region can be empty in one dimension. If a single row is left, toptop has already passed bottombottom after the first walk, so the bottom walk must be skipped — otherwise it re-emits that row backwards. The included [[1,2,3]][[1,2,3]] and [[1],[2],[3]][[1],[2],[3]] cases fail loudly without the guards, which is exactly why they are in the test.

An alternative that some people find easier to defend: keep a directions = [(0,1),(1,0),(0,-1),(-1,0)]directions = [(0,1),(1,0),(0,-1),(-1,0)] cycle and a visitedvisited set, turning right whenever the next cell is out of bounds or already seen. It is O(mn)O(m \cdot n) space rather than O(1)O(1), but it has no boundary arithmetic at all — a reasonable trade if you are more confident with it.

Follow-ups you should expect: “Generate a spiral instead of reading one (LC 59)?” — identical walk, assign 1..n²1..n² as you go. “Spiral counter-clockwise?” — reorder the four walks. “Diagonal order (LC 498)?” — different structure entirely: cells with equal r + cr + c form a diagonal, so bucket by that sum and reverse alternate buckets.

LC 73 — Set Matrix Zeroes · Medium

Problem. Given an m x nm x n matrix, if an element is 00, set its entire row and column to 00. Do it in place.

Constraints. 1 <= m, n <= 2001 <= m, n <= 200, -2^31 <= matrix[i][j] <= 2^31 - 1-2^31 <= matrix[i][j] <= 2^31 - 1. Note that any sentinel value you might pick is a legal matrix entry.

Examples. [[1,1,1],[1,0,1],[1,1,1]][[1,1,1],[1,0,1],[1,1,1]] gives [[1,0,1],[0,0,0],[1,0,1]][[1,0,1],[0,0,0],[1,0,1]] · [[0,1,2,0],[3,4,5,2],[1,3,1,5]][[0,1,2,0],[3,4,5,2],[1,3,1,5]] gives [[0,0,0,0],[0,4,5,0],[0,3,1,0]][[0,0,0,0],[0,4,5,0],[0,3,1,0]]

Editorial — approach, complexity, follow-ups

Start from the O(m+n)O(m + n) solution and say it out loud — it is a perfectly good answer and shows you have a working baseline: collect zero rows and columns in two sets, then apply them.

To reach O(1)O(1), store those flags in the matrix’s own first row and column. Two things make this delicate:

  1. The borders are real data too. Before using them as storage, record whether they originally contained a zero. Two booleans, not one — and matrix[0][0]matrix[0][0] alone cannot represent both.
  2. Order matters. Zero the interior first, then the borders. If you zero row 0 early, every column looks marked and the whole matrix goes to zero.

Time O(mn)O(m \cdot n). Space O(1)O(1).

Follow-ups you should expect: “Do the O(m+n)O(m+n) version first” — often what they actually want before optimising. “What if you may not modify the input at all?” — then O(m+n)O(m+n) sets and a fresh output matrix. “Set rows and columns to 11 where you find a 11?” — same structure, but check whether the marker and target values now collide.

LeetCode problem set

#ProblemDifficultyThe twist
867Transpose MatrixEasyNon-square, so you must allocate — zip(*matrix)zip(*matrix) does it in one line
766Toeplitz MatrixEasyEvery cell equals its up-left neighbour; one comparison per cell
48Rotate ImageMediumTranspose then reverse rows; inner loop from i + 1i + 1
54Spiral MatrixMediumFour boundaries and the two degenerate-case guards
59Spiral Matrix IIMediumThe same walk, writing 1..n²1..n² instead of reading
73Set Matrix ZeroesMediumBorders as marker storage; no sentinel value is safe
498Diagonal TraverseMediumCells on a diagonal share r + cr + c; reverse alternate diagonals
289Game of LifeMediumSimultaneous update — encode old and new state in two bits

Interview follow-ups

They askWhat they’re checkingThe answer
“Why does transpose + reverse equal a rotation?”Whether you can justify itTranspose maps (r,c)(r,c) to (c,r)(c,r); reversing rows then maps that to (c, n-1-r)(c, n-1-r), which is the clockwise rotation
“Rotate counter-clockwise / 180°?”Whether you memorised or understoodTranspose + reverse row order; or reverse rows and row order with no transpose
“Can you avoid the extra sets in LC 73?”Space optimisationStore the flags in row 0 and column 0, with two booleans for the borders themselves
“Why not a sentinel value?”Reading the constraintsEntries span the full int range, so every sentinel is a possible real value
“Non-square rotation?”Boundary thinkingDimensions swap, so in-place is impossible; allocate the result
O(1)O(1) space with neighbour-based updates?”Bit tricksEncode next state in higher bits (LC 289), then shift in a second pass
“How do you avoid off-by-one in the spiral?”Method, not luckName the four boundaries, shrink after each walk, and test single-row and single-column inputs

Edge-case checklist

  • 1×1 matrix — every pattern must survive it.
  • Single row [[1,2,3]][[1,2,3]] and single column [[1],[2],[3]][[1],[2],[3]] — the spiral guards exist for these; test both, always.
  • Empty matrix [][] or [[]][[]] — guard before reading matrix[0]matrix[0].
  • Non-square — fine for spiral and transpose, impossible for in-place rotation.
  • A zero in row 0 or column 0 (LC 73) — the case the two booleans exist for.
  • All zeros / no zeros (LC 73) — the trivial ends of the range.
  • Even vs odd nn (LC 48) — an odd nn has a fixed centre cell; make sure your loop bounds neither skip nor double-swap it.
  • Duplicate values — never identify a cell by its value; use indices.

Recap

  • Rotation in place = transpose, then reverse each row. Inner loop from i + 1i + 1, or you swap everything twice and achieve nothing.
  • Spiral traversal = four boundaries, shrink after each walk, and two guards so a lone remaining row or column is not walked twice.
  • O(1)O(1) space for row/column flags = store them in row 0 and column 0, keep two booleans for the borders, and write the borders last.
  • When updates depend on neighbours’ original values, either use two passes or encode both states in one cell.
  • No sentinel value is safe when the constraints allow the full integer range.
  • Rearranging a grid is this page; searching a grid is BFS/DFS on grids.

Next: Monotonic Stack — the pattern behind next-greater-element and largest-rectangle problems.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did