Given an m x n matrix, return all of its elements in spiral order: start at the top-left, move right across the top row, down the right column, left across the bottom row, up the left column, and continue spiralling inward until every element is visited.
Example
Right across the top, down the right edge, left along the bottom, up the left edge, then the center.
Constraints
- m == matrix.length
- n == matrix[i].length
- 1 <= m, n <= 10
- -100 <= matrix[i][j] <= 100
Intuition
A direct way to trace the spiral is to walk the grid like a robot: keep moving in the current direction, and only turn — clockwise — the moment the next cell would leave the grid or has already been visited, tracking every visited cell in its own boolean grid.
function spiralOrder(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
const result = [];
// Remember every cell already emitted — "already visited" is how we know when to turn.
const visited = Array.from({ length: rows }, () => new Array(cols).fill(false));
// Clockwise order: right, down, left, up.
const directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
let dir = 0;
let row = 0;
let col = 0;
for (let step = 0; step < rows * cols; step++) {
result.push(matrix[row][col]);
visited[row][col] = true;
const [dr, dc] = directions[dir];
const nextRow = row + dr;
const nextCol = col + dc;
// Turn clockwise the instant the next cell falls off the grid or was already emitted.
const blocked = nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || visited[nextRow][nextCol];
if (blocked) dir = (dir + 1) % 4;
row += directions[dir][0];
col += directions[dir][1];
}
return result;
}This still visits every cell exactly once — O(m·n) time either way — but it pays for an O(m·n) visited grid just to answer one question at each step: have I been here before? Can we do better?
The key observation: the spiral doesn't need to ask that per cell, because it only ever turns at the same four moments each lap — right after finishing the top row, the right column, the bottom row, and the left column. So instead of remembering which cells are done, track which rows and columns are done, with four numbers: top, bottom, left, right. Walk one full edge along each bound, shrink that bound inward by one, and repeat until the bounds cross. It's the same "has this region already been consumed" bookkeeping that shows up across Matrices & grids ring-walk problems (matrix rotation, border traversal) — just four numbers standing in for a full visited grid.
The one wrinkle is a matrix that isn't square: after the top-row and right-column passes, a single remaining row or column would otherwise get walked twice — once as the "bottom row", again as the "left column" — unless those two passes are guarded with if (top <= bottom) and if (left <= right) and skipped once nothing is left along that edge.
Walking it through:
matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
Walk the current top row left to right, then move the top bound down past it.
Walk the right column top to bottom — row 0's corner was already taken — then pull the right bound in.
The guard passes: top hasn't crossed bottom yet, so the bottom row still has cells the first pass never touched.
Same guard on the other axis: left hasn't crossed right, so there's one cell left above the corner already taken.
The bounds now describe a single remaining row — a 1×2 sliver. It's still a valid ring, so the same top-row pass walks it.
The right-column and left-column passes run but find nothing left in their range, and the `if (top <= bottom)` guard now evaluates false — exactly the check that stops the bottom row from being walked a second time on a matrix that isn't square. Result: 1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7.
Optimization
Shrinking boundaries
Keep four boundary indices — top, bottom, left, right — and peel one edge at a time: the top row (left→right), the right column (top→bottom), the bottom row (right→left), the left column (bottom→top). After each edge, move that boundary inward. The two inner walks are guarded so a single remaining row or column isn't visited twice.
O(m·n) time, O(1) extra space beyond the output.
function spiralOrder(matrix) {
const out = [];
// Four bounds mark the current outer ring; each shrinks inward once its edge is walked.
let top = 0;
let bottom = matrix.length - 1;
let left = 0;
let right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// Top row, left to right.
for (let j = left; j <= right; j++) out.push(matrix[top][j]);
top++;
// Right column, top to bottom (row "top" is now one past the row just consumed).
for (let i = top; i <= bottom; i++) out.push(matrix[i][right]);
right--;
// Guard: a single remaining row would already be exhausted by the two passes above.
if (top <= bottom) {
// Bottom row, right to left.
for (let j = right; j >= left; j--) out.push(matrix[bottom][j]);
bottom--;
}
// Guard: a single remaining column would already be exhausted by the passes above.
if (left <= right) {
// Left column, bottom to top.
for (let i = bottom; i >= top; i--) out.push(matrix[i][left]);
left++;
}
}
return out;
}Complexity analysis
Time complexity: O(m·n). Here's why:
- Each of the four edge passes (top row, right column, bottom row, left column) advances a bound that only ever shrinks, so across the whole run each bound moves inward at most
morntimes total. - Together the four passes partition the grid into concentric rings, and every cell is pushed to the output exactly once as its ring is walked.
So the total work is proportional to the cell count: O(m·n), where m and n are the matrix's dimensions. We can't do better — any correct solution has to look at every cell at least once.
Space complexity: O(1) extra, not counting the output. Here's why:
- The brute-force baseline keeps a
visitedboolean grid with one entry per cell — O(m·n) extra space. - The stored solution drops that grid entirely: it tracks only the four bound variables
top,bottom,left,right— O(1) extra space, regardless of the matrix's size.
So the optimization trades the O(m·n) visited grid for O(1) extra space. The only space that scales with the input is the out array holding the m × n result values, which every correct solution has to produce.
Test cases
Beyond the example above, these are worth thinking through before you submit.
| Input | Expected output | Description |
|---|---|---|
| matrix = 42 | [42] | 1×1 matrix — the top-row pass emits the only cell, then both bounds immediately cross. |
| matrix = 102030 | [10,20,30] | A single row: only the top-row pass ever fires; the guarded bottom-row pass never runs because bottom < top right after it. |
| matrix = 1 2 3 4 | [1,2,3,4] | A single column: the top-row pass takes one cell, then the (unguarded) right-column pass walks the rest top to bottom. |
| matrix = 55 55 | [5,5,5,5] | Every value identical — the walk is purely positional, so four indistinguishable values still come out in the right order. |
| matrix = 121 212 | [1,2,1,2,1,2] | Repeated values scattered across the grid — the algorithm tracks indices, not values, so duplicates don't confuse the walk. |
Try it yourself
Write your solution against the real judge before checking the reference.