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 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:
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]]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:
| Rotation | Recipe |
|---|---|
| 90° clockwise | transpose, then reverse each row |
| 90° counter-clockwise | transpose, 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.
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]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 solution: collect the row and column indices in two sets, then apply. The -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
| Variant | The move | Canonical problem |
|---|---|---|
| Rotate 90° | Transpose, then reverse rows | 48 Rotate Image |
| Transpose (non-square) | Must allocate — dimensions change | 867 Transpose Matrix |
| Spiral read | Four boundaries + two guards | 54 Spiral Matrix |
| Spiral write | Same walk, assigning 1..n²1..n² instead of reading | 59 Spiral Matrix II |
| Diagonal walk | Cells on a diagonal share r + cr + c; bucket by that sum | 498 Diagonal Traverse |
| Flag in place | Use row 0 / column 0 as marker storage | 73 Set Matrix Zeroes |
| Simultaneous update | Encode next state in spare bits, or use a second value | 289 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 — unavoidable, every element moves. Space .
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 . Space 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 space rather than , 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 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 , store those flags in the matrix’s own first row and column. Two things make this delicate:
- 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. - 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 . Space .
Follow-ups you should expect: “Do the version first” — often
what they actually want before optimising. “What if you may not modify the
input at all?” — then 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
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 867 | Transpose Matrix | Easy | Non-square, so you must allocate — zip(*matrix)zip(*matrix) does it in one line |
| 766 | Toeplitz Matrix | Easy | Every cell equals its up-left neighbour; one comparison per cell |
| 48 | Rotate Image | Medium | Transpose then reverse rows; inner loop from i + 1i + 1 |
| 54 | Spiral Matrix | Medium | Four boundaries and the two degenerate-case guards |
| 59 | Spiral Matrix II | Medium | The same walk, writing 1..n²1..n² instead of reading |
| 73 | Set Matrix Zeroes | Medium | Borders as marker storage; no sentinel value is safe |
| 498 | Diagonal Traverse | Medium | Cells on a diagonal share r + cr + c; reverse alternate diagonals |
| 289 | Game of Life | Medium | Simultaneous update — encode old and new state in two bits |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Why does transpose + reverse equal a rotation?” | Whether you can justify it | Transpose 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 understood | Transpose + reverse row order; or reverse rows and row order with no transpose |
| “Can you avoid the extra sets in LC 73?” | Space optimisation | Store the flags in row 0 and column 0, with two booleans for the borders themselves |
| “Why not a sentinel value?” | Reading the constraints | Entries span the full int range, so every sentinel is a possible real value |
| “Non-square rotation?” | Boundary thinking | Dimensions swap, so in-place is impossible; allocate the result |
| ” space with neighbour-based updates?” | Bit tricks | Encode 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 luck | Name 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 readingmatrix[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 oddnnhas 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.
- 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 coffeeWas this page helpful?
Let us know how we did
