TL;DR
Walk four shrinking boundaries (top/bottom/left/right) inward β O(mΒ·n) time, O(1) extra space beyond the output.
Approach 1 β Direction simulation with a visited grid
The insight: you can literally βwalkβ the spiral. Move in the current direction (right β down β left β up, cycling) and turn whenever the next cell is off the grid or already seen. A boolean visited grid tells you when to turn.
from typing import List
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
visited = [[False] * n for _ in range(m)]
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # R, D, L, U
r = c = d = 0
result = []
for _ in range(m * n):
result.append(matrix[r][c])
visited[r][c] = True
nr, nc = r + directions[d][0], c + directions[d][1]
if 0 <= nr < m and 0 <= nc < n and not visited[nr][nc]:
r, c = nr, nc
else:
d = (d + 1) % 4 # turn clockwise
r += directions[d][0]
c += directions[d][1]
return result
Complexity: O(mΒ·n) time, O(mΒ·n) extra space for visited. Simple, but it allocates a second grid.
Approach 2 β Four shrinking boundaries (O(1) space)
The insight: you donβt need a visited grid β the four edges of the not-yet-emitted rectangle are the state. Keep top, bottom, left, right; emit the top row and drop it (top += 1), emit the right column and drop it (right -= 1), and so on. Re-check the loop condition between passes so a final lone row or column isnβt traversed twice.
from typing import List
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
m, n = len(matrix), len(matrix[0])
top, bottom, left, right = 0, m - 1, 0, n - 1
result = []
while top <= bottom and left <= right:
for c in range(left, right + 1): # top row, left β right
result.append(matrix[top][c])
top += 1
for r in range(top, bottom + 1): # right column, top β bottom
result.append(matrix[r][right])
right -= 1
if top <= bottom: # bottom row, right β left
for c in range(right, left - 1, -1):
result.append(matrix[bottom][c])
bottom -= 1
if left <= right: # left column, bottom β top
for r in range(bottom, top - 1, -1):
result.append(matrix[r][left])
left += 1
return result
Walkthrough with [[1,2,3,4],[5,6,7,8],[9,10,11,12]] (top=0,bottom=2,left=0,right=3):
- Top row
1,2,3,4 β top=1.
- Right column (rows 1..2, col 3)
8,12 β right=2.
top(1) <= bottom(2): bottom row (cols 2..0) 11,10,9 β bottom=1.
left(0) <= right(2): left column (rows 1..1, col 0) 5 β left=1.
- Loop again (
top=1<=bottom=1, left=1<=right=2): top row (cols 1..2) 6,7 β top=2.
- Right column:
top(2) > bottom(1) range empty β right=1.
top(2) <= bottom(1) false, skip. left(1) <= right(1)β¦ but now top > bottom so outer loop ends.
- Result:
[1,2,3,4,8,12,11,10,9,5,6,7]. Matches.
Complexity: O(mΒ·n) time, O(1) extra space beyond the returned list.
Common pitfalls
- Omitting the
if top <= bottom / if left <= right guards before the bottom row and left column β on a single leftover row or column youβd emit it twice.
- Off-by-one in the reversed ranges: bottom row goes
range(right, left - 1, -1); the -1 bounds are easy to botch.
- Assuming a square matrix β
m and n differ, so never reuse one dimension for both.
- Updating a boundary at the wrong time (before emitting its edge instead of after).
Pattern takeaway
For layered grid traversals, represent the frontier with a handful of boundary indices and shrink them as you consume each edge β no auxiliary grid needed. The recurring guard is to re-test the loop condition between the horizontal and vertical passes, since the innermost layer can degenerate to a single line.