Appearance
4.28.2 — Spiral Matrix
LeetCode 54 · Medium
The problem
Return every element of the matrix in spiral order.
1 2 3
4 5 6 → [1,2,3,6,9,8,7,4,5]
7 8 9The matrix may be rectangular, not just square.
The pattern
Track four boundaries — top, bottom, left, right — and peel one edge at a time, shrinking the boundary you just consumed.
→ → → walk the TOP row, then top++
↑ ↓ walk the RIGHT col, then right--
↑ ↓ walk the BOTTOM row, then bottom--
← ← ← walk the LEFT col, then left++Repeat until the boundaries cross.
The difficulty is entirely in the boundary checks, not the idea. On a matrix with one row left, the bottom pass would walk the same row again backwards; on one column left, the left pass would repeat it. Both need guarding.
The two guards
After the top row and the right column are consumed, check before doing the other two:
- Before the bottom row: only if
top <= bottom. A single remaining row was already taken by the top pass. - Before the left column: only if
left <= right. A single remaining column was already taken by the right pass.
Miss these and elements are emitted twice — and it only shows up on rectangular matrices or at the very centre, which is why this problem catches people who tested on a 3×3.
The solution
python
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix:
return []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
result = []
while top <= bottom and left <= right:
for c in range(left, right + 1): # top row, left to right
result.append(matrix[top][c])
top += 1
for r in range(top, bottom + 1): # right column, top to bottom
result.append(matrix[r][right])
right -= 1
if top <= bottom: # guard: rows remain
for c in range(right, left - 1, -1): # bottom row, right to left
result.append(matrix[bottom][c])
bottom -= 1
if left <= right: # guard: columns remain
for r in range(bottom, top - 1, -1): # left column, bottom to top
result.append(matrix[r][left])
left += 1
return resultts
function spiralOrder(matrix: number[][]): number[] {
if (!matrix.length) return [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
const result: number[] = [];
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) result.push(matrix[top][c]);
top++;
for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
right--;
if (top <= bottom) {
for (let c = right; c >= left; c--) result.push(matrix[bottom][c]);
bottom--;
}
if (left <= right) {
for (let r = bottom; r >= top; r--) result.push(matrix[r][left]);
left++;
}
}
return result;
}The boundary is shrunk immediately after each pass, so the next pass starts from the correct place. Doing all four passes and then adjusting all four boundaries produces overlaps.
The top and right passes need no guard, because the outer while has just confirmed both boundaries are valid.
The bottom and left passes do, because top++ and right-- may have crossed the boundaries in the meantime.
The 1×3 case
[[1,2,3]] — this is the input that exposes a missing guard.
topandbottomare both 0;left0,right2.- Top pass emits
1, 2, 3.topbecomes 1. - Right pass:
range(1, 1)is empty, so nothing.rightbecomes 1. - Bottom guard:
top (1) <= bottom (0)is false → skipped. Without this guard it would emit2, 1again. - Left guard:
left (0) <= right (1)is true, but the looprange(0, 0, -1)is empty.
Result [1,2,3] ✓. Test your solution on a single row and a single column before you say you are done.
Complexity
O(mn) time — every element is emitted once. O(1) extra space beyond the output.
The direction-vector alternative
A different formulation: keep a direction vector and turn right whenever the next cell is out of bounds or already visited.
python
directions = [(0,1), (1,0), (0,-1), (-1,0)] # right, down, left, up
d = 0
r = c = 0
for _ in range(rows * cols):
result.append(matrix[r][c])
matrix[r][c] = None # mark visited
nr, nc = r + directions[d][0], c + directions[d][1]
if not (0 <= nr < rows and 0 <= nc < cols) or matrix[nr][nc] is None:
d = (d + 1) % 4 # turn right
nr, nc = r + directions[d][0], c + directions[d][1]
r, c = nr, nc(d + 1) % 4 cycling through four direction vectors is a genuinely useful idiom — it appears in robot-simulation problems and grid walks throughout this book.
It needs to mark visited cells, which either mutates the input or costs O(mn) space. The boundary version is cleaner here, but the direction-vector idea is the one that transfers.
Where this goes next
- Spiral Matrix II — fill an
n × nmatrix with 1 ton²in spiral order. Same loop, writing instead of reading. - Spiral Matrix III — spiral outwards from a point, possibly leaving the grid. The direction-vector form is the natural fit, with the step length growing 1, 1, 2, 2, 3, 3, …
- Rotate Image — a different traversal of the same grid. 4.28.1.
What the interviewer will push on
"Where are the guards and why?" Before the bottom and left passes, because a single remaining row or column has already been consumed.
"Test it on a 1×n matrix." Do this unprompted; it is the case the problem is designed around.
"Why shrink the boundary immediately?" So the next pass starts in the right place.
"What if the matrix is empty?" The early return, and note that matrix[0] would throw without it.
"Is there another formulation?" Direction vectors with (d + 1) % 4, at the cost of marking visited cells.
One thing to volunteer: walk the 1×3 case out loud before writing code. It shows you know where the bug lives rather than discovering it from a failing test.
Next: 4.28.3 Set Matrix Zeroes — in-place again, using the matrix itself as scratch space.