Spiral Matrix — Boundary Shrinking Mastery [Google, Microsoft, Amazon]
Advertisement
Problem Statement
Given an
m x nmatrix, return all elements of the matrix in spiral order.
Example 1:
Input:
[[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
Output: [1, 2, 3, 6, 9, 8, 7, 4, 5]
Example 2:
Input:
[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]]
Output: [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
Constraints:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100
- Why This Problem Matters
- Two Approaches
- Approach 1: Boundary Shrinking (Recommended)
- Approach 2: Direction Array (Bonus)
- The Boundary Approach in Detail
- Pass 1 — Walk Right Along the Top Row
- Pass 2 — Walk Down Along the Right Column
- Pass 3 — Walk Left Along the Bottom Row (guarded)
- Pass 4 — Walk Up Along the Left Column (guarded)
- Edge Cases
- Single Row: [[1, 2, 3, 4]]
- Single Column: [[1], [2], [3]]
- 1×1 Matrix: [[42]]
- Visual Dry Run — [[1,2,3],[4,5,6],[7,8,9]]
- Common Mistakes
- Mistake 1: Forgetting the boundary guards between directions
- Mistake 2: Shrinking boundaries in the wrong order
- Mistake 3: Off-by-one in the reverse range
- Mistake 4: Using visited grid when boundary shrinking suffices
- Solutions
- Python — Boundary Shrinking (Primary)
- Python — Direction Array (Bonus)
- JavaScript — Boundary Shrinking (Primary)
- JavaScript — Direction Array (Bonus)
- Complexity Analysis
- Follow-Up Questions
- Spiral Matrix II — LeetCode 59
- Diagonal Traverse — LeetCode 498
- Rotate Matrix by 90 Degrees — LeetCode 48
- This Pattern Solves
- Key Takeaway
Why This Problem Matters
Spiral Matrix is the #18 most frequently asked LeetCode problem globally, appearing in interviews at Google, Amazon, Microsoft, Meta, Goldman Sachs, Bloomberg, Databricks, JP Morgan, and Capital One — 60 occurrences across 24 companies. That is not a coincidence.
This problem is deceptively simple to describe but surprisingly hard to code cleanly under pressure. The reason interviewers love it:
It is a pure simulation problem. There is no clever trick, no DP table, no graph traversal. You simply walk around the matrix in a spiral. The challenge is entirely in managing four boundaries simultaneously without confusion, off-by-one errors, or double-counting cells.
What interviewers are really evaluating:
- State management — Can you track and update four variables (
top,bottom,left,right) correctly through an entire loop? - Boundary guards — Do you remember to check if boundaries are still valid between directional passes, not just at the start of the loop?
- Code clarity — Is your logic readable? Can you explain why each shrink happens at each step?
- Edge case awareness — Do you handle 1×n (single row), m×1 (single column), and 1×1 grids without special-casing every path?
The best candidates solve this cleanly in under 15 minutes and can articulate each decision. The boundary shrinking approach makes all of this achievable.
Two Approaches
Approach 1: Boundary Shrinking (Recommended)
Maintain four integer pointers — top, bottom, left, right — representing the active layer of the matrix. Walk right across the top row, then down the right column, then left across the bottom row, then up the left column. After each pass, shrink that boundary inward. Repeat until the boundaries collapse.
Why this works: Each "layer" of the spiral is exactly one traversal of all four walls. Shrinking a boundary after walking along it ensures you never revisit that row or column.
Time: O(m × n) — every cell is visited exactly once.
Space: O(1) extra — the result array is the required output, not auxiliary space.
Approach 2: Direction Array (Bonus)
Maintain a current direction using dx/dy delta arrays (right → down → left → up). Walk in the current direction until you either hit the matrix boundary or land on an already-visited cell, then rotate direction 90 degrees clockwise.
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # right, down, left, up
This approach requires a visited boolean grid (O(m×n) space), making it slightly heavier. It is more flexible for unusual traversal shapes but harder to reason about during an interview because the rotation logic adds mental overhead.
Bottom line: Use boundary shrinking in interviews. It is more readable, requires no extra space, and maps directly to the problem's structure.
The Boundary Approach in Detail
Initialize the four walls:
top = 0 (first row index)
bottom = m - 1 (last row index)
left = 0 (first column index)
right = n - 1 (last column index)
Enter a while top <= bottom and left <= right loop. Inside, four passes happen in sequence:
Pass 1 — Walk Right Along the Top Row
for c in range(left, right + 1):
result.append(matrix[top][c])
top += 1 # this row is consumed, shrink top inward
Collect every cell in row top from column left to right. After finishing, increment top — that entire row has been consumed and should never be visited again.
Pass 2 — Walk Down Along the Right Column
for r in range(top, bottom + 1):
result.append(matrix[r][right])
right -= 1 # this column is consumed, shrink right inward
Collect every cell in column right from row top (already incremented) to bottom. After finishing, decrement right.
Pass 3 — Walk Left Along the Bottom Row (guarded)
if top <= bottom: # critical guard!
for c in range(right, left - 1, -1):
result.append(matrix[bottom][c])
bottom -= 1
Why the guard? After passes 1 and 2, if the matrix was a single row, top has already advanced past bottom. Without this check, you would traverse the top row again in reverse — a double-count bug. The if top <= bottom guard prevents this.
Pass 4 — Walk Up Along the Left Column (guarded)
if left <= right: # critical guard!
for r in range(bottom, top - 1, -1):
result.append(matrix[r][left])
left += 1
Same idea. If the matrix was a single column, right has already dropped below left. The guard prevents a second pass over the same column.
After all four passes, the outer layer is complete. The boundaries have shrunk inward by one on all four sides. The loop continues until the boundaries fully collapse.
Edge Cases
Single Row: [[1, 2, 3, 4]]
- Initial:
top=0, bottom=0, left=0, right=3 - Pass 1 (right): collects
[1, 2, 3, 4], thentop = 1 - Pass 2 (down):
range(1, 1)is empty — nothing collected - Pass 3 (left):
top (1) > bottom (0)→ guard triggers, skipped - Pass 4 (up):
left (0) <= right (3)→range(0, 1-1, -1)=range(0, 0, -1)is empty - Loop condition:
top (1) > bottom (0)→ exits
Output: [1, 2, 3, 4] — correct.
Single Column: [[1], [2], [3]]
- Initial:
top=0, bottom=2, left=0, right=0 - Pass 1 (right): collects
[1],top = 1 - Pass 2 (down): collects
[2, 3],right = -1 - Pass 3 (left):
top (1) <= bottom (2)— guard passes.range(-1, -1, -1)is empty.bottom = 1 - Pass 4 (up):
left (0) > right (-1)→ guard triggers, skipped - Loop condition:
left (0) > right (-1)→ exits
Output: [1, 2, 3] — correct.
1×1 Matrix: [[42]]
- Pass 1: collects
[42],top = 1 - All guards trigger or ranges are empty
- Loop exits immediately on next check
Output: [42] — correct.
The boundary approach handles all three edge cases without any special-casing. The guards and the empty-range behavior of Python/JavaScript range/loops do the heavy lifting.
Visual Dry Run — [[1,2,3],[4,5,6],[7,8,9]]
Let us trace every step with boundary values shown explicitly.
Initial state:
top=0, bottom=2, left=0, right=2
[1] [2] [3]
[4] [5] [6]
[7] [8] [9]
result = []
Iteration 1, Pass 1 (Walk Right — row 0, cols 0→2):
top=0, bottom=2, left=0, right=2
Collect: 1, 2, 3 → top becomes 1
result = [1, 2, 3]
Iteration 1, Pass 2 (Walk Down — col 2, rows 1→2):
top=1, bottom=2, left=0, right=2
Collect: 6, 9 → right becomes 1
result = [1, 2, 3, 6, 9]
Iteration 1, Pass 3 (Walk Left — row 2, cols 1→0) [guard: top(1) <= bottom(2) ✓]:
top=1, bottom=2, left=0, right=1
Collect: 8, 7 → bottom becomes 1
result = [1, 2, 3, 6, 9, 8, 7]
Iteration 1, Pass 4 (Walk Up — col 0, rows 1→1) [guard: left(0) <= right(1) ✓]:
top=1, bottom=1, left=0, right=1
Collect: 4 → left becomes 1
result = [1, 2, 3, 6, 9, 8, 7, 4]
Iteration 2 begins — loop check: top(1) <= bottom(1) ✓, left(1) <= right(1) ✓
Iteration 2, Pass 1 (Walk Right — row 1, col 1→1):
top=1, bottom=1, left=1, right=1
Collect: 5 → top becomes 2
result = [1, 2, 3, 6, 9, 8, 7, 4, 5]
Iteration 2, Pass 2 (Walk Down — col 1, rows 2→1):
range(2, 2) is empty — nothing collected
right becomes 0
Iteration 2, Pass 3: guard top(2) > bottom(1) → SKIPPED
Iteration 2, Pass 4: guard left(1) > right(0) → SKIPPED
Loop check: top(2) > bottom(1) → EXIT
Final result: [1, 2, 3, 6, 9, 8, 7, 4, 5] — matches expected output perfectly.
The ASCII spiral path for reference:
→ → →
↓
← ← ↓
↑ ↓
↑ ← ← (center: 5)
More precisely:
1 → 2 → 3
↓
4 5 6 ←
↓
7 ← 8 ← 9
Wait — the actual spiral:
1 → 2 → 3
↓
4 ← 5 6
↑ (visiting right col: 6→9)
↓ ↓
7 → 8 → 9
Cleaner representation:
[1]→[2]→[3]
|
[4] [5] [6]
↑ |
[7]←[8]←[9]
Then inner: [4] picked up going left→right, [5] last.
Common Mistakes
Mistake 1: Forgetting the boundary guards between directions
The most frequent bug. After walking right (Pass 1) and incrementing top, you must check top <= bottom before walking left (Pass 3). Without this, a single-row matrix produces duplicate output.
# WRONG — no guard on Pass 3
for c in range(right, left - 1, -1):
result.append(matrix[bottom][c])
# CORRECT
if top <= bottom:
for c in range(right, left - 1, -1):
result.append(matrix[bottom][c])
Mistake 2: Shrinking boundaries in the wrong order
The shrink must happen immediately after the corresponding pass, before the next pass begins. Moving a shrink line to the wrong position means the next pass uses stale boundary values.
# WRONG — shrinking top before using it in Pass 2
top += 1
for r in range(top + 1, bottom + 1): # off by one!
...
# CORRECT — shrink top immediately after Pass 1, then Pass 2 uses updated top
for c in range(left, right + 1):
result.append(matrix[top][c])
top += 1 # ← shrink here
for r in range(top, bottom + 1): # Pass 2 now correctly skips consumed top row
result.append(matrix[r][right])
Mistake 3: Off-by-one in the reverse range
When walking left (Pass 3), the range must go down to left inclusive. The Python expression is range(right, left - 1, -1). Using range(right, left, -1) misses the leftmost element.
# WRONG — misses matrix[bottom][left]
for c in range(right, left, -1): # stops before left
# CORRECT
for c in range(right, left - 1, -1): # includes left
Mistake 4: Using visited grid when boundary shrinking suffices
Some candidates reach for a visited boolean grid to avoid re-traversal. This works but costs O(m×n) extra space. The boundary approach eliminates the need entirely — shrinking boundaries implicitly marks cells as "done."
Solutions
Python — Boundary Shrinking (Primary)
def spiralOrder(matrix: list[list[int]]) -> list[int]:
result = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# Pass 1: Walk right across the top row
for c in range(left, right + 1):
result.append(matrix[top][c])
top += 1 # top row consumed
# Pass 2: Walk down the right column
for r in range(top, bottom + 1):
result.append(matrix[r][right])
right -= 1 # right column consumed
# Pass 3: Walk left across the bottom row
# Guard: needed when matrix has only one row remaining
if top <= bottom:
for c in range(right, left - 1, -1):
result.append(matrix[bottom][c])
bottom -= 1 # bottom row consumed
# Pass 4: Walk up the left column
# Guard: needed when matrix has only one column remaining
if left <= right:
for r in range(bottom, top - 1, -1):
result.append(matrix[r][left])
left += 1 # left column consumed
return result
Python — Direction Array (Bonus)
def spiralOrder(matrix: list[list[int]]) -> list[int]:
if not matrix:
return []
m, n = len(matrix), len(matrix[0])
visited = [[False] * n for _ in range(m)]
result = []
# right, down, left, up
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
direction_idx = 0
row, col = 0, 0
for _ in range(m * n):
result.append(matrix[row][col])
visited[row][col] = True
dr, dc = directions[direction_idx]
next_row, next_col = row + dr, col + dc
# Rotate direction if out of bounds or already visited
if (
not (0 <= next_row < m and 0 <= next_col < n)
or visited[next_row][next_col]
):
direction_idx = (direction_idx + 1) % 4
dr, dc = directions[direction_idx]
next_row, next_col = row + dr, col + dc
row, col = next_row, next_col
return result
# Time: O(m*n) Space: O(m*n) for visited grid
JavaScript — Boundary Shrinking (Primary)
/**
* @param {number[][]} matrix
* @return {number[]}
*/
function spiralOrder(matrix) {
const result = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// Pass 1: Walk right across the top row
for (let c = left; c <= right; c++) {
result.push(matrix[top][c]);
}
top++; // top row consumed
// Pass 2: Walk down the right column
for (let r = top; r <= bottom; r++) {
result.push(matrix[r][right]);
}
right--; // right column consumed
// Pass 3: Walk left across the bottom row
// Guard against double-traversal on single-row matrices
if (top <= bottom) {
for (let c = right; c >= left; c--) {
result.push(matrix[bottom][c]);
}
bottom--; // bottom row consumed
}
// Pass 4: Walk up the left column
// Guard against double-traversal on single-column matrices
if (left <= right) {
for (let r = bottom; r >= top; r--) {
result.push(matrix[r][left]);
}
left++; // left column consumed
}
}
return result;
}
JavaScript — Direction Array (Bonus)
function spiralOrder(matrix) {
const m = matrix.length, n = matrix[0].length;
const visited = Array.from({ length: m }, () => new Array(n).fill(false));
const result = [];
// [row_delta, col_delta]: right, down, left, up
const directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
let dirIdx = 0;
let row = 0, col = 0;
for (let i = 0; i < m * n; i++) {
result.push(matrix[row][col]);
visited[row][col] = true;
const [dr, dc] = directions[dirIdx];
const nextRow = row + dr, nextCol = col + dc;
// Rotate if out of bounds or already visited
if (
nextRow < 0 || nextRow >= m ||
nextCol < 0 || nextCol >= n ||
visited[nextRow][nextCol]
) {
dirIdx = (dirIdx + 1) % 4;
}
row += directions[dirIdx][0];
col += directions[dirIdx][1];
}
return result;
// Time: O(m*n) Space: O(m*n) for visited grid
}
Complexity Analysis
| Boundary Shrinking | Direction Array | |
|---|---|---|
| Time | O(m × n) | O(m × n) |
| Space | O(1) | O(m × n) |
| Interview preference | Preferred | Acceptable |
Every cell is visited exactly once in both approaches. The boundary shrinking approach uses only four integer variables as auxiliary state — the result array is the required output, not extra space.
Follow-Up Questions
Spiral Matrix II — LeetCode 59
Given an integer n, generate an n × n matrix filled with elements from 1 to n² in spiral order.
def generateMatrix(n: int) -> list[list[int]]:
matrix = [[0] * n for _ in range(n)]
top, bottom, left, right = 0, n - 1, 0, n - 1
num = 1
while top <= bottom and left <= right:
for c in range(left, right + 1):
matrix[top][c] = num; num += 1
top += 1
for r in range(top, bottom + 1):
matrix[r][right] = num; num += 1
right -= 1
if top <= bottom:
for c in range(right, left - 1, -1):
matrix[bottom][c] = num; num += 1
bottom -= 1
if left <= right:
for r in range(bottom, top - 1, -1):
matrix[r][left] = num; num += 1
left += 1
return matrix
Spiral Matrix II uses the exact same boundary logic — the only difference is writing num into the matrix instead of reading from it.
Diagonal Traverse — LeetCode 498
Return all elements of an m × n matrix in diagonal order (alternating up-right and down-left diagonals).
Key insight: every cell (i, j) belongs to diagonal d = i + j. Group cells by their diagonal index. Diagonals with even index go up-right; odd index go down-left. This is another direction-tracking simulation.
def findDiagonalOrder(matrix):
if not matrix:
return []
m, n = len(matrix), len(matrix[0])
diagonals = {}
for i in range(m):
for j in range(n):
d = i + j
diagonals.setdefault(d, []).append(matrix[i][j])
result = []
for d in sorted(diagonals):
if d % 2 == 0:
result.extend(reversed(diagonals[d]))
else:
result.extend(diagonals[d])
return result
Rotate Matrix by 90 Degrees — LeetCode 48
Rotate an n×n matrix in-place. Combine transpose (flip across main diagonal) with reverse each row. This is another pure boundary manipulation problem that tests the same in-place state management skills.
This Pattern Solves
The boundary shrinking / direction simulation pattern appears in a family of matrix traversal problems:
- Rotate Image (LC 48) — in-place 90° rotation using layer-by-layer boundary swaps
- Set Matrix Zeroes (LC 73) — using boundary markers to propagate information
- Game of Life (LC 289) — simulate next state without extra space using in-place encoding
- Zigzag Conversion (LC 6) — direction-flip simulation on a 1D string
- Number of Islands (LC 200) — boundary-aware BFS/DFS expansion
- Valid Sudoku (LC 36) — region boundary indexing (
3*(r//3),3*(c//3))
Any time you are asked to traverse a structure in a non-standard order, think: What are my boundaries? What is my direction state? How do I update both cleanly?
Key Takeaway
Spiral Matrix is the quintessential boundary simulation problem — the kind where no clever algorithm saves you, and success comes entirely from clean state management. Master the four-pointer shrink pattern: walk right, shrink top; walk down, shrink right; walk left (guarded), shrink bottom; walk up (guarded), shrink left. The two guards between Pass 2 and Passes 3–4 are what separate correct solutions from almost-correct solutions on non-square matrices. Once this mental model is locked in, Spiral Matrix II becomes a one-line variation, and any future matrix traversal problem becomes a template-fill exercise.
Advertisement