Intuition
A 90° clockwise rotation equals two simple in-place moves: transpose the matrix (swap across the main diagonal), then reverse each row. Together they send every element to its rotated position.
Transpose, then reverse each row — two simple passes replacing one confusing four-way cycle. Recognising that a rotation decomposes into a reflection across the diagonal followed by a horizontal flip is the trick worth keeping; the anticlockwise rotation is the same pair with the reverse applied to columns instead.
Approach
Copying into a new matrix is the easy way out
You can allocate a fresh n×n grid and place each element at its rotated coordinate using a formula. That's straightforward but uses O(n²) extra space, which the in-place requirement forbids. The insight is that a rotation can be decomposed into two simpler in-place operations that compose to the same result.
A 90° rotation = transpose + reverse rows
Transposing the matrix swaps matrix[i][j] with matrix[j][i], turning rows into columns — this gets every element onto the correct line but mirrored. Reversing each row afterward flips that mirror, landing every element in its final rotated position. It's worth convincing yourself on a small example: together these two flips equal a clockwise quarter-turn.
Transpose carefully, then reverse
Do the transpose with the inner loop starting at j = i + 1, so each off-diagonal pair is swapped exactly once — swapping the full grid would undo itself. Then reverse each row in place. Both steps touch each cell a constant number of times: O(n²) time, O(1) extra space. A 1×1 grid is a no-op, as a rotation should be.
Solution & live demo
Common pitfalls
Transposing over the full index range
for i in range(n):
for j in range(n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]for i in range(n):
for j in range(i + 1, n):Every pair is swapped twice — once as (i, j) and once as (j, i) — which returns the matrix to its original state. Iterating only above the diagonal touches each pair exactly once.
Reversing before transposing
for row in matrix: row.reverse() # then transpose
# transpose for row in matrix: row.reverse()
The two operations don't commute — reversing first produces a 90° anticlockwise rotation. Order is the entire difference between the two directions.
Building a new matrix
return [list(row) for row in zip(*matrix[::-1])]
# swap in place, then reverse rows
Elegant, but the problem requires modifying the input in place with O(1) extra space. Returning a new grid also leaves the caller's matrix unchanged, so the expected output never appears.
Edge cases
Transpose and row-reverse are both no-ops; the single cell is unchanged, as a rotation should leave it.
One diagonal swap plus two row reversals produce the correct 90° turn.
Restricting the transpose to j > i ensures each off-diagonal pair is swapped exactly once.