A clear guide to reading a matrix in spiral order using shrinking boundaries.
Problem Restatement
We are given an m x n matrix.
We need to return all elements of the matrix in spiral order.
The spiral starts at the top-left cell. It moves:
- Right across the top row
- Down the right column
- Left across the bottom row
- Up the left column
Then it repeats the same pattern on the inner part of the matrix.
The official constraints are 1 <= m, n <= 10 and -100 <= matrix[i][j] <= 100.
Input and Output
| Item | Meaning |
|---|---|
| Input | A 2D integer matrix |
| Output | A list of integers in spiral order |
| Matrix size | m x n |
| Traversal | Visit each cell exactly once |
Function shape:
def spiralOrder(matrix: list[list[int]]) -> list[int]:
...Examples
For:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]The spiral order is:
[1, 2, 3, 6, 9, 8, 7, 4, 5]We first read the top row:
1, 2, 3Then the right column:
6, 9Then the bottom row from right to left:
8, 7Then the left column from bottom to top:
4Finally, the center:
5For:
matrix = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
]The answer is:
[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]First Thought: Simulate Movement
One way is to simulate the actual movement.
Start at (0, 0).
Move right until the next cell is outside the matrix or already visited. Then turn down. Then turn left. Then turn up. Repeat until all cells have been visited.
This works, but it needs a visited matrix or it needs to modify the input matrix.
There is a cleaner approach.
Key Insight
A spiral traversal reads the matrix layer by layer.
Each layer has four boundaries:
| Boundary | Meaning |
|---|---|
top | First unvisited row |
bottom | Last unvisited row |
left | First unvisited column |
right | Last unvisited column |
For each layer:
- Read the top row from left to right.
- Move
topdown. - Read the right column from top to bottom.
- Move
rightleft. - Read the bottom row from right to left, if it still exists.
- Move
bottomup. - Read the left column from bottom to top, if it still exists.
- Move
leftright.
The checks before reading the bottom row and left column matter because rectangular matrices can collapse into a single row or a single column.
Algorithm
Initialize:
top = 0
bottom = len(matrix) - 1
left = 0
right = len(matrix[0]) - 1
ans = []While the boundaries are valid:
top <= bottom and left <= rightRead in four directions:
- Top row: columns
lefttoright. - Right column: rows
toptobottom. - Bottom row: columns
righttoleft, only iftop <= bottom. - Left column: rows
bottomtotop, only ifleft <= right.
Return ans.
Correctness
The algorithm keeps four boundaries around the unvisited part of the matrix.
Reading the top row visits every cell on the current top edge exactly once, then top moves inward. Reading the right column visits every cell on the current right edge exactly once, then right moves inward.
After those two moves, the remaining unvisited region may have no rows or no columns. The algorithm checks the boundaries before reading the bottom row and left column. This prevents duplicate visits in single-row and single-column cases.
Each loop removes one outer layer from the unvisited rectangle. Since every cell belongs to exactly one such layer, every cell is added once. The order of adding cells follows right, down, left, and up, so the returned list is exactly the spiral order.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(m * n) | Every cell is visited once |
| Space | O(1) extra | Apart from the output list, only boundary variables are stored |
The output list stores m * n values, which is required by the problem.
Implementation
class Solution:
def spiralOrder(self, matrix: list[list[int]]) -> list[int]:
top = 0
bottom = len(matrix) - 1
left = 0
right = len(matrix[0]) - 1
ans = []
while top <= bottom and left <= right:
for col in range(left, right + 1):
ans.append(matrix[top][col])
top += 1
for row in range(top, bottom + 1):
ans.append(matrix[row][right])
right -= 1
if top <= bottom:
for col in range(right, left - 1, -1):
ans.append(matrix[bottom][col])
bottom -= 1
if left <= right:
for row in range(bottom, top - 1, -1):
ans.append(matrix[row][left])
left += 1
return ansCode Explanation
We start with the whole matrix as the unvisited rectangle:
top = 0
bottom = len(matrix) - 1
left = 0
right = len(matrix[0]) - 1The loop continues while there is still a valid rectangle:
while top <= bottom and left <= right:First, read the top row:
for col in range(left, right + 1):
ans.append(matrix[top][col])
top += 1After reading it, that row is consumed, so top moves down.
Next, read the right column:
for row in range(top, bottom + 1):
ans.append(matrix[row][right])
right -= 1After reading it, that column is consumed, so right moves left.
Before reading the bottom row, check that a row still remains:
if top <= bottom:Then read from right to left:
for col in range(right, left - 1, -1):
ans.append(matrix[bottom][col])
bottom -= 1Before reading the left column, check that a column still remains:
if left <= right:Then read from bottom to top:
for row in range(bottom, top - 1, -1):
ans.append(matrix[row][left])
left += 1When the boundaries cross, all cells have been visited.
Testing
def run_tests():
s = Solution()
assert s.spiralOrder([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]) == [1, 2, 3, 6, 9, 8, 7, 4, 5]
assert s.spiralOrder([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
]) == [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
assert s.spiralOrder([[1]]) == [1]
assert s.spiralOrder([[1, 2, 3]]) == [1, 2, 3]
assert s.spiralOrder([
[1],
[2],
[3],
]) == [1, 2, 3]
assert s.spiralOrder([
[1, 2],
[3, 4],
]) == [1, 2, 4, 3]
print("all tests passed")
run_tests()| Test | Why |
|---|---|
3 x 3 matrix | Standard square case |
3 x 4 matrix | Rectangular case |
1 x 1 matrix | Smallest input |
| Single row | Prevents duplicate bottom-row traversal |
| Single column | Prevents duplicate left-column traversal |
2 x 2 matrix | Small even-sized square case |