54. Spiral Matrix
Problem Description
You are given a 2D matrix with m
rows and n
columns. Your task is to traverse the matrix in a spiral pattern and return all elements in the order they are visited.
A spiral traversal starts from the top-left corner of the matrix and moves:
- First, to the right across the top row
- Then down along the right column
- Then left across the bottom row
- Then up along the left column
- This pattern continues inward until all elements are visited
For example, given a 3×3 matrix:
1 2 3 4 5 6 7 8 9
The spiral order would be: [1, 2, 3, 6, 9, 8, 7, 4, 5]
The traversal follows the path: start at 1 → move right to 2, 3 → move down to 6, 9 → move left to 8, 7 → move up to 4 → move right to 5.
The solution uses a simulation approach with direction vectors. It maintains the current position (i, j)
and tracks visited cells using a boolean matrix vis
. The direction is controlled by an index k
that cycles through four directions: right (0, 1)
, down (1, 0)
, left (0, -1)
, and up (-1, 0)
. When the next move would go out of bounds or hit an already visited cell, the direction changes clockwise by incrementing k
. This process continues until all m × n
elements are collected in the result list.
Intuition
When we think about traversing a matrix in spiral order, we can visualize it like peeling an onion layer by layer, or like walking along the walls of a rectangular room and gradually moving inward.
The key insight is that a spiral traversal follows a fixed pattern of directions: right → down → left → up, and this pattern repeats. Whenever we hit a boundary (either the edge of the matrix or a cell we've already visited), we need to turn 90 degrees clockwise.
This naturally leads us to think about using direction vectors. We can represent the four directions as coordinate changes:
- Right:
(0, 1)
- row stays same, column increases - Down:
(1, 0)
- row increases, column stays same - Left:
(0, -1)
- row stays same, column decreases - Up:
(-1, 0)
- row decreases, column stays same
By storing these directions in a sequence like dirs = (0, 1, 0, -1, 0)
, we can cleverly access both x and y components of each direction using consecutive indices. For direction k
, the row change is dirs[k]
and column change is dirs[k+1]
.
To track our progress and know when to turn, we need to mark cells as visited. This prevents us from revisiting cells and helps us identify when we've hit the "inner wall" of previously traversed cells. When our next move would either go out of bounds or hit a visited cell, we know it's time to change direction.
The beauty of this approach is its simplicity - we just keep moving in the current direction until we can't, then rotate clockwise and continue. By doing this for exactly m × n
steps, we're guaranteed to visit every cell exactly once in spiral order.
Solution Approach
The implementation uses a simulation strategy to traverse the matrix in spiral order. Here's how the solution works step by step:
1. Initialize the necessary variables:
m, n
store the dimensions of the matrixdirs = (0, 1, 0, -1, 0)
encodes the four directions. Each pair(dirs[k], dirs[k+1])
represents a direction vectorvis
is a 2D boolean array of the same size as the matrix to track visited cells, initialized toFalse
i, j
represent the current position, starting at(0, 0)
k
is the direction index, starting at 0 (moving right)ans
is the result list to store elements in spiral order
2. Main traversal loop:
The loop runs exactly m × n
times to visit every cell:
for _ in range(m * n):
ans.append(matrix[i][j]) # Add current element to result
vis[i][j] = True # Mark current cell as visited
3. Calculate the next position:
x, y = i + dirs[k], j + dirs[k + 1]
This computes where we would move if we continue in the current direction.
4. Check if direction change is needed:
if x < 0 or x >= m or y < 0 or y >= n or vis[x][y]: k = (k + 1) % 4
We need to turn clockwise (change direction) if:
- The next position is out of bounds (
x < 0
orx >= m
ory < 0
ory >= n
) - The next position has already been visited (
vis[x][y]
isTrue
)
The modulo operation (k + 1) % 4
ensures we cycle through directions 0→1→2→3→0...
5. Move to the next position:
i += dirs[k] j += dirs[k + 1]
After potentially changing direction, we update our position using the current (possibly new) direction.
The algorithm guarantees that we visit each cell exactly once because:
- We track visited cells to avoid revisiting
- We change direction only when necessary
- The loop runs for exactly the total number of cells
Time complexity is O(m × n)
as we visit each cell once, and space complexity is O(m × n)
for the visited array and result list.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small 3×3 matrix to see how the spiral traversal works:
Matrix: 1 2 3 4 5 6 7 8 9
Initial Setup:
- Position:
(i, j) = (0, 0)
- Direction index:
k = 0
(moving right) dirs = (0, 1, 0, -1, 0)
where direction vectors are:- k=0: right (0, 1)
- k=1: down (1, 0)
- k=2: left (0, -1)
- k=3: up (-1, 0)
- Visited matrix: all
False
- Result:
[]
Step 1: At (0,0), value = 1
- Add 1 to result:
[1]
- Mark (0,0) as visited
- Next position: (0,0) + (0,1) = (0,1) ✓ valid
- Move to (0,1)
Step 2: At (0,1), value = 2
- Add 2 to result:
[1, 2]
- Mark (0,1) as visited
- Next position: (0,1) + (0,1) = (0,2) ✓ valid
- Move to (0,2)
Step 3: At (0,2), value = 3
- Add 3 to result:
[1, 2, 3]
- Mark (0,2) as visited
- Next position: (0,2) + (0,1) = (0,3) ✗ out of bounds!
- Change direction: k = 1 (now moving down)
- New next position: (0,2) + (1,0) = (1,2)
- Move to (1,2)
Step 4: At (1,2), value = 6
- Add 6 to result:
[1, 2, 3, 6]
- Mark (1,2) as visited
- Next position: (1,2) + (1,0) = (2,2) ✓ valid
- Move to (2,2)
Step 5: At (2,2), value = 9
- Add 9 to result:
[1, 2, 3, 6, 9]
- Mark (2,2) as visited
- Next position: (2,2) + (1,0) = (3,2) ✗ out of bounds!
- Change direction: k = 2 (now moving left)
- New next position: (2,2) + (0,-1) = (2,1)
- Move to (2,1)
Step 6: At (2,1), value = 8
- Add 8 to result:
[1, 2, 3, 6, 9, 8]
- Mark (2,1) as visited
- Next position: (2,1) + (0,-1) = (2,0) ✓ valid
- Move to (2,0)
Step 7: At (2,0), value = 7
- Add 7 to result:
[1, 2, 3, 6, 9, 8, 7]
- Mark (2,0) as visited
- Next position: (2,0) + (0,-1) = (2,-1) ✗ out of bounds!
- Change direction: k = 3 (now moving up)
- New next position: (2,0) + (-1,0) = (1,0)
- Move to (1,0)
Step 8: At (1,0), value = 4
- Add 4 to result:
[1, 2, 3, 6, 9, 8, 7, 4]
- Mark (1,0) as visited
- Next position: (1,0) + (-1,0) = (0,0) ✗ already visited!
- Change direction: k = 0 (now moving right)
- New next position: (1,0) + (0,1) = (1,1)
- Move to (1,1)
Step 9: At (1,1), value = 5
- Add 5 to result:
[1, 2, 3, 6, 9, 8, 7, 4, 5]
- Mark (1,1) as visited
- All cells visited!
Final Result: [1, 2, 3, 6, 9, 8, 7, 4, 5]
The traversal creates a clockwise spiral, changing direction whenever hitting a boundary or visited cell, ensuring every element is collected exactly once.
Solution Implementation
1class Solution:
2 def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
3 # Get matrix dimensions
4 num_rows, num_cols = len(matrix), len(matrix[0])
5
6 # Direction vectors: right(0,1), down(1,0), left(0,-1), up(-1,0)
7 # Stored as (dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4) in compressed form
8 directions = (0, 1, 0, -1, 0)
9
10 # Track visited cells to know when to turn
11 visited = [[False] * num_cols for _ in range(num_rows)]
12
13 # Initialize position (row, col) and direction index
14 row = col = direction_idx = 0
15 result = []
16
17 # Traverse all cells in the matrix
18 for _ in range(num_rows * num_cols):
19 # Add current cell value to result
20 result.append(matrix[row][col])
21 visited[row][col] = True
22
23 # Calculate next position based on current direction
24 next_row = row + directions[direction_idx]
25 next_col = col + directions[direction_idx + 1]
26
27 # Check if we need to change direction (hit boundary or visited cell)
28 if (next_row < 0 or next_row >= num_rows or
29 next_col < 0 or next_col >= num_cols or
30 visited[next_row][next_col]):
31 # Turn clockwise (right -> down -> left -> up -> right)
32 direction_idx = (direction_idx + 1) % 4
33
34 # Move to next position using current (possibly updated) direction
35 row += directions[direction_idx]
36 col += directions[direction_idx + 1]
37
38 return result
39
1class Solution {
2 public List<Integer> spiralOrder(int[][] matrix) {
3 // Get matrix dimensions
4 int rows = matrix.length;
5 int cols = matrix[0].length;
6
7 // Direction vectors: right(0,1), down(1,0), left(0,-1), up(-1,0)
8 // Stored as: [rowDelta1, colDelta1, rowDelta2, colDelta2, ...]
9 int[] directions = {0, 1, 0, -1, 0};
10
11 // Current position and direction index
12 int currentRow = 0;
13 int currentCol = 0;
14 int directionIndex = 0; // 0=right, 1=down, 2=left, 3=up
15
16 // Result list to store spiral order elements
17 List<Integer> result = new ArrayList<>();
18
19 // Track visited cells to know when to turn
20 boolean[][] visited = new boolean[rows][cols];
21
22 // Traverse all cells in the matrix
23 int totalCells = rows * cols;
24 for (int count = 0; count < totalCells; count++) {
25 // Add current cell value to result
26 result.add(matrix[currentRow][currentCol]);
27
28 // Mark current cell as visited
29 visited[currentRow][currentCol] = true;
30
31 // Calculate next position based on current direction
32 int nextRow = currentRow + directions[directionIndex];
33 int nextCol = currentCol + directions[directionIndex + 1];
34
35 // Check if we need to change direction (hit boundary or visited cell)
36 if (nextRow < 0 || nextRow >= rows ||
37 nextCol < 0 || nextCol >= cols ||
38 visited[nextRow][nextCol]) {
39 // Turn clockwise to next direction
40 directionIndex = (directionIndex + 1) % 4;
41 }
42
43 // Move to next position using current direction
44 currentRow += directions[directionIndex];
45 currentCol += directions[directionIndex + 1];
46 }
47
48 return result;
49 }
50}
51
1class Solution {
2public:
3 vector<int> spiralOrder(vector<vector<int>>& matrix) {
4 int rows = matrix.size();
5 int cols = matrix[0].size();
6
7 // Direction vectors: right, down, left, up
8 // dirs[i] and dirs[i+1] represent row and column increments respectively
9 int directions[5] = {0, 1, 0, -1, 0};
10
11 // Current position and direction index
12 int currentRow = 0;
13 int currentCol = 0;
14 int directionIndex = 0;
15
16 // Result vector to store spiral order elements
17 vector<int> result;
18
19 // Visited matrix to track already visited cells
20 bool visited[rows][cols];
21 memset(visited, false, sizeof(visited));
22
23 // Traverse all elements in the matrix
24 for (int count = rows * cols; count > 0; --count) {
25 // Add current element to result
26 result.push_back(matrix[currentRow][currentCol]);
27
28 // Mark current cell as visited
29 visited[currentRow][currentCol] = true;
30
31 // Calculate next position based on current direction
32 int nextRow = currentRow + directions[directionIndex];
33 int nextCol = currentCol + directions[directionIndex + 1];
34
35 // Check if we need to change direction:
36 // - If next position is out of bounds
37 // - If next position is already visited
38 if (nextRow < 0 || nextRow >= rows ||
39 nextCol < 0 || nextCol >= cols ||
40 visited[nextRow][nextCol]) {
41 // Change direction clockwise (right -> down -> left -> up -> right)
42 directionIndex = (directionIndex + 1) % 4;
43 }
44
45 // Move to next position using current (possibly updated) direction
46 currentRow += directions[directionIndex];
47 currentCol += directions[directionIndex + 1];
48 }
49
50 return result;
51 }
52};
53
1/**
2 * Traverses a matrix in spiral order (clockwise) and returns elements as an array
3 * @param matrix - 2D array of numbers to traverse
4 * @returns Array containing matrix elements in spiral order
5 */
6function spiralOrder(matrix: number[][]): number[] {
7 const rows: number = matrix.length;
8 const cols: number = matrix[0].length;
9 const result: number[] = [];
10
11 // Track visited cells to avoid revisiting
12 const visited: boolean[][] = Array.from(
13 { length: rows },
14 () => Array(cols).fill(false)
15 );
16
17 // Direction vectors: right, down, left, up (clockwise)
18 // dirs[i] and dirs[i+1] represent row and column deltas respectively
19 const directionDeltas: number[] = [0, 1, 0, -1, 0];
20
21 // Initialize position and direction
22 let currentRow: number = 0;
23 let currentCol: number = 0;
24 let directionIndex: number = 0; // 0=right, 1=down, 2=left, 3=up
25
26 // Total number of elements to process
27 const totalElements: number = rows * cols;
28
29 // Process each element in the matrix
30 for (let count: number = 0; count < totalElements; count++) {
31 // Add current element to result
32 result.push(matrix[currentRow][currentCol]);
33 visited[currentRow][currentCol] = true;
34
35 // Calculate next position based on current direction
36 const nextRow: number = currentRow + directionDeltas[directionIndex];
37 const nextCol: number = currentCol + directionDeltas[directionIndex + 1];
38
39 // Check if we need to change direction (hit boundary or visited cell)
40 if (nextRow < 0 || nextRow >= rows ||
41 nextCol < 0 || nextCol >= cols ||
42 visited[nextRow][nextCol]) {
43 // Rotate clockwise to next direction
44 directionIndex = (directionIndex + 1) % 4;
45 }
46
47 // Move to next position using current (possibly updated) direction
48 currentRow += directionDeltas[directionIndex];
49 currentCol += directionDeltas[directionIndex + 1];
50 }
51
52 return result;
53}
54
Time and Space Complexity
Time Complexity: O(m × n)
The algorithm visits each element in the matrix exactly once. The main loop runs m × n
times (where m
is the number of rows and n
is the number of columns), and in each iteration, it performs constant-time operations: appending to the result list, marking the cell as visited, calculating the next position, and checking boundaries. Therefore, the overall time complexity is O(m × n)
.
Space Complexity: O(m × n)
The space complexity consists of two main components:
- The
vis
(visited) matrix, which has the same dimensions as the input matrix, requiringO(m × n)
space to track which cells have been visited. - The
ans
list, which stores allm × n
elements from the matrix, also requiringO(m × n)
space.
Since both components require O(m × n)
space, the total space complexity is O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Direction Array Indexing
A common mistake is incorrectly accessing the direction vectors when using the compressed format dirs = (0, 1, 0, -1, 0)
.
Pitfall Example:
# Incorrect: Using wrong indices for direction vectors next_row = row + directions[direction_idx * 2] # Wrong! next_col = col + directions[direction_idx * 2 + 1] # Wrong!
The compressed array stores directions as consecutive pairs, but the indices are [0,1]
, [1,2]
, [2,3]
, [3,4]
, not [0,1]
, [2,3]
, [4,5]
, [6,7]
.
Solution:
# Correct: Proper indexing for compressed direction array next_row = row + directions[direction_idx] next_col = col + directions[direction_idx + 1]
2. Updating Position Before Checking All Conditions
Pitfall Example:
for _ in range(num_rows * num_cols):
result.append(matrix[row][col])
visited[row][col] = True
# Wrong: Update position immediately
row += directions[direction_idx]
col += directions[direction_idx + 1]
# Then check if out of bounds (but we've already moved!)
if row < 0 or row >= num_rows or col < 0 or col >= num_cols or visited[row][col]:
# Try to correct by backing out and changing direction
row -= directions[direction_idx]
col -= directions[direction_idx + 1]
direction_idx = (direction_idx + 1) % 4
row += directions[direction_idx]
col += directions[direction_idx + 1]
This approach is error-prone and can lead to accessing invalid indices.
Solution: Always calculate the next position first, validate it, then update:
# Calculate potential next position next_row = row + directions[direction_idx] next_col = col + directions[direction_idx + 1] # Check if we need to turn if (next_row < 0 or next_row >= num_rows or next_col < 0 or next_col >= num_cols or visited[next_row][next_col]): direction_idx = (direction_idx + 1) % 4 # Now safely move to the next position row += directions[direction_idx] col += directions[direction_idx + 1]
3. Forgetting to Handle Edge Cases
Pitfall Example: Not handling single row or single column matrices properly, or assuming the matrix is always square.
# Wrong assumption: Matrix is always square
for _ in range(num_rows * num_rows): # Should be num_rows * num_cols!
...
Solution: Always use both dimensions correctly:
# Correct: Handle any m×n matrix
for _ in range(num_rows * num_cols):
...
4. Incorrect Visited Array Initialization
Pitfall Example:
# Wrong: Creates references to the same list visited = [[False] * num_cols] * num_rows # Don't do this! # This creates num_rows references to the same list # Modifying visited[0][0] will affect all rows!
Solution: Use list comprehension to create independent rows:
# Correct: Each row is a separate list
visited = [[False] * num_cols for _ in range(num_rows)]
5. Not Handling Empty Matrix
Pitfall Example:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
num_rows, num_cols = len(matrix), len(matrix[0]) # IndexError if matrix is empty!
Solution: Add a guard clause:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix or not matrix[0]:
return []
num_rows, num_cols = len(matrix), len(matrix[0])
# ... rest of the code
Which data structure is used to implement priority queue?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!