You're given an m × n matrix — a grid of numbers with m rows and n columns. Return every element in clockwise spiral order: start at the top-left, walk right across the top row, down the right column, left across the bottom row, up the left column, then spiral inward one ring at a time until every cell is visited. Think of peeling an onion layer by layer, or a camera panning around the outer edge of a photo and then tightening toward the center.
// matrix: number[][] — an m×n grid. Rows all have the same length n.
// The values can be anything; the tests use numbers.
// returns: number[] — every element, in clockwise spiral order,
// starting from matrix[0][0].
function matrixSpiralTraversal(matrix: number[][]): number[];
// 3×3 — full clockwise loop, then the single center cell.
matrixSpiralTraversal([
[1, 2, 3],
[8, 9, 4],
[7, 6, 5],
]);
// → [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 3×4 wide rectangle — the outer ring, then the inner [11, 12] strip.
matrixSpiralTraversal([
[1, 2, 3, 4],
[10, 11, 12, 5],
[9, 8, 7, 6],
]);
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
m × n shape is allowed — square, wide, or tall. The output length always equals m × n (every cell is visited exactly once).[]. Both [] (no rows) and [[]] (one row with no columns) produce an empty array.1 × n matrix is just its row left-to-right; an m × 1 matrix is just its column top-to-bottom. The tricky part is making sure these aren't visited twice.matrix untouched.You'll flatten an m × n grid into one array by walking its cells in a clockwise spiral, peeling off one ring at a time from the outside in.
Imagine a spreadsheet and you want to read every cell, but not row-by-row — instead you trace the outer border clockwise, then step inside and trace the next border, and keep tightening until you hit the middle. That path is the spiral: right across the top, down the right side, left along the bottom, up the left side, then inward. The output is just the values in the order your finger touches them. The whole challenge is bookkeeping: knowing where each of the four passes starts and stops, and not touching the same cell twice when the leftover region is a single thin line.
Picture four walls closing in: a top row index, a bottom row index, a left column index, and a right column index. They start at the four edges of the matrix and fence in the rectangle of cells you still have to visit. Every time you finish a pass, you push the wall you just walked along one step toward the center. When top crosses past bottom, or left crosses past right, the fenced region is empty and you're done.
This four-boundaries picture is the entire algorithm. There's no visited grid, no direction vectors — just four integers and four loops, in a fixed clockwise order, running until the walls cross.
Before the boundary approach, the move most people reach for is direction-based walking with a visited set. You stand on a cell, keep stepping in your current direction, and turn clockwise whenever the next step would leave the grid or land on a cell you've already marked. It works — it's just heavier than it needs to be.
function spiralWalk(matrix) {
if (matrix.length === 0 || matrix[0].length === 0) return [];
const rows = matrix.length;
const cols = matrix[0].length;
const result = [];
const visited = new Set(); // keys like "r,c"
// Clockwise direction order: right, down, left, up.
const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]];
let r = 0, c = 0, d = 0;
for (let i = 0; i < rows * cols; i++) {
result.push(matrix[r][c]);
visited.add(`${r},${c}`);
// Compute the next cell in the current direction.
let nr = r + dirs[d][0];
let nc = c + dirs[d][1];
// Turn clockwise if the next cell is off-grid or already seen.
if (
nr < 0 || nr >= rows || nc < 0 || nc >= cols ||
visited.has(`${nr},${nc}`)
) {
d = (d + 1) % 4; // rotate right → down → left → up → right
nr = r + dirs[d][0];
nc = c + dirs[d][1];
}
r = nr;
c = nc;
}
return result;
}
This produces the correct answer. But look at the cost. Every cell allocates a string key ("r,c") and does a Set insert plus a Set lookup on the turn check — that's hashing strings for all m × n cells. The visited set is O(m × n) extra memory just to re-derive a shape the matrix already has: a rectangle. The boundary method below tracks the same "what's left" information in four integers, with zero hashing and zero extra allocation. Same answer, a fraction of the work — and arguably easier to reason about, because the turn logic is replaced by four plain loops in a fixed order.
function matrixSpiralTraversal(matrix) {
const result = [];
// Empty grid OR a grid whose rows have no columns ([[]]) → nothing to visit.
if (matrix.length === 0 || matrix[0].length === 0) return result;
// Four walls fencing in the not-yet-visited rectangle, inclusive on both ends.
let top = 0;
let bottom = matrix.length - 1;
let left = 0;
let right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 1. Top row, left → right.
for (let col = left; col <= right; col++) {
result.push(matrix[top][col]);
}
top++; // top row consumed; close the wall in from above
// 2. Right column, top → bottom.
for (let row = top; row <= bottom; row++) {
result.push(matrix[row][right]);
}
right--; // right column consumed; close in from the right
// 3. Bottom row, right → left — ONLY if a row still remains.
if (top <= bottom) {
for (let col = right; col >= left; col--) {
result.push(matrix[bottom][col]);
}
bottom--;
}
// 4. Left column, bottom → top — ONLY if a column still remains.
if (left <= right) {
for (let row = bottom; row >= top; row--) {
result.push(matrix[row][left]);
}
left++;
}
}
return result;
}
module.exports = { matrixSpiralTraversal };
The shift from the naive version is that the visited set is gone entirely. The four walls are the memory: any cell with top ≤ row ≤ bottom and left ≤ col ≤ right is unvisited; everything outside that box is done. Each pass walks one wall and then moves it, so the box strictly shrinks every iteration and the loop is guaranteed to terminate.
Two choices deserve a closer look. First, the boundaries are inclusive — bottom = matrix.length - 1, not matrix.length. That's why every loop uses <= / >= and why the bottom and left passes count down to left and up to top. Getting one of those comparisons wrong by a single step is the classic off-by-one that either drops a corner cell or reads matrix[bottom][-1].
Second, and this is the heart of the question, passes 3 and 4 are guarded by if (top <= bottom) and if (left <= right). The top and right passes (1 and 2) never need a guard because the while condition already proved top <= bottom && left <= right when the iteration began. But pass 1 increments top and pass 2 decrements right, so by the time we reach passes 3 and 4 the walls may have already crossed. Without those two guards, a matrix whose final leftover band is a single row or single column gets that line visited twice.
Let's trace the 3×4 rectangle from the prompt, because it's the smallest shape where the single-line guard actually fires:
matrix = [
[ 1, 2, 3, 4],
[10, 11, 12, 5],
[ 9, 8, 7, 6],
]
Initial walls: top=0, bottom=2, left=0, right=3. The while condition 0 <= 2 && 0 <= 3 holds, so we enter the first ring.
Ring 1
Pass 1 (top row 0, cols 0→3): push 1, 2, 3, 4 → top becomes 1
Pass 2 (right col 3, rows 1→2): push 5, 6 → right becomes 2
Pass 3 (guard 1<=2 ✓; row 2, cols 2→0): push 7, 8, 9 → bottom becomes 1
Pass 4 (guard 0<=2 ✓; col 0, rows 1→1): push 10 → left becomes 1
result so far: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After ring 1 the walls are top=1, bottom=1, left=1, right=2. The while check 1 <= 1 && 1 <= 2 still holds — there's an inner band left — so we go again.
Ring 2
Pass 1 (top row 1, cols 1→2): push 11, 12 → top becomes 2
Pass 2 (right col 2, rows 2→1): loop body never runs (2 > 1, empty range)
→ right becomes 1
Pass 3 (guard: top=2 <= bottom=1 ? NO → SKIP)
Pass 4 (guard: left=1 <= right=1 ? YES; col 1, rows 1→2: empty range)
result so far: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Here's the guard doing its job. After pass 1 of ring 2, top is 2 while bottom is still 1. Pass 3 asks top <= bottom — that's 2 <= 1, which is false, so the bottom pass is skipped. If it had run, it would have walked row = bottom = 1 from right back to left and re-pushed 12, 11 — a double-visit of the exact cells pass 1 just emitted. Pass 4's guard left <= right is 1 <= 1 (true), but its inner loop runs row from bottom=1 down to top=2, which is an empty range, so nothing is pushed. The walls have now crossed: the next while check 2 <= 1 is false, the loop exits, and we return the 12 values in spiral order.
The boundary shrink itself — all four walls stepping inward after one ring — looks like this:
top past bottom. If pass 3 isn't guarded by if (top <= bottom), it walks the same row backward and you get duplicates like [..., 11, 12, 12, 11]. The same happens on a single leftover column without if (left <= right) on pass 4. Both guards are mandatory, not optional polish.top before pass 2 reads the right column, pass 2 starts one row too low; if you decrement right before pass 1, pass 1 stops one column short. Walk-then-shrink, one wall at a time.bottom and right are the last valid index (length - 1), so the loops are <= / >=. A < where you needed <= silently drops the final cell of a pass (the corner gets skipped); going one past turns into an out-of-bounds read like matrix[row][-1], which is undefined. Pick inclusive and stay consistent across all four loops.matrixSpiralTraversal([]) has matrix.length === 0; matrixSpiralTraversal([[]]) has matrix[0].length === 0. Both must short-circuit to [] before you read matrix[0].length, because evaluating matrix[0].length on [] would read undefined.length and throw. Check matrix.length === 0 first so the || short-circuits.m > n), wide (m < n), and square (m === n) all run the identical loop. People sometimes write a separate branch for non-square inputs; you don't need one. The inclusive walls and the two guards already cover every shape, including 1 × n and m × 1.1 × n matrix: pass 1 emits the whole row, top becomes 1 > bottom (0). Pass 2's range is empty. Pass 3 is correctly skipped by its guard. A m × 1 matrix: pass 1 emits one cell, pass 2 emits the rest of the column, and the bottom/left guards keep the lone column from being re-read. These degenerate shapes are exactly why the guards exist.n × n). The inverse problem: given n, produce the matrix [[1,2,3],[8,9,4],[7,6,5]] where the numbers 1..n² spiral inward. Same four-wall loop, but instead of pushing matrix[r][c] you assign matrix[r][c] = counter++. Pre-allocate the grid, run the identical traversal, write instead of read.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're given an m × n matrix — a grid of numbers with m rows and n columns. Return every element in clockwise spiral order: start at the top-left, walk right across the top row, down the right column, left across the bottom row, up the left column, then spiral inward one ring at a time until every cell is visited. Think of peeling an onion layer by layer, or a camera panning around the outer edge of a photo and then tightening toward the center.
// matrix: number[][] — an m×n grid. Rows all have the same length n.
// The values can be anything; the tests use numbers.
// returns: number[] — every element, in clockwise spiral order,
// starting from matrix[0][0].
function matrixSpiralTraversal(matrix: number[][]): number[];
// 3×3 — full clockwise loop, then the single center cell.
matrixSpiralTraversal([
[1, 2, 3],
[8, 9, 4],
[7, 6, 5],
]);
// → [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 3×4 wide rectangle — the outer ring, then the inner [11, 12] strip.
matrixSpiralTraversal([
[1, 2, 3, 4],
[10, 11, 12, 5],
[9, 8, 7, 6],
]);
// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
m × n shape is allowed — square, wide, or tall. The output length always equals m × n (every cell is visited exactly once).[]. Both [] (no rows) and [[]] (one row with no columns) produce an empty array.1 × n matrix is just its row left-to-right; an m × 1 matrix is just its column top-to-bottom. The tricky part is making sure these aren't visited twice.matrix untouched.You'll flatten an m × n grid into one array by walking its cells in a clockwise spiral, peeling off one ring at a time from the outside in.
Imagine a spreadsheet and you want to read every cell, but not row-by-row — instead you trace the outer border clockwise, then step inside and trace the next border, and keep tightening until you hit the middle. That path is the spiral: right across the top, down the right side, left along the bottom, up the left side, then inward. The output is just the values in the order your finger touches them. The whole challenge is bookkeeping: knowing where each of the four passes starts and stops, and not touching the same cell twice when the leftover region is a single thin line.
Picture four walls closing in: a top row index, a bottom row index, a left column index, and a right column index. They start at the four edges of the matrix and fence in the rectangle of cells you still have to visit. Every time you finish a pass, you push the wall you just walked along one step toward the center. When top crosses past bottom, or left crosses past right, the fenced region is empty and you're done.
This four-boundaries picture is the entire algorithm. There's no visited grid, no direction vectors — just four integers and four loops, in a fixed clockwise order, running until the walls cross.
Before the boundary approach, the move most people reach for is direction-based walking with a visited set. You stand on a cell, keep stepping in your current direction, and turn clockwise whenever the next step would leave the grid or land on a cell you've already marked. It works — it's just heavier than it needs to be.
function spiralWalk(matrix) {
if (matrix.length === 0 || matrix[0].length === 0) return [];
const rows = matrix.length;
const cols = matrix[0].length;
const result = [];
const visited = new Set(); // keys like "r,c"
// Clockwise direction order: right, down, left, up.
const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]];
let r = 0, c = 0, d = 0;
for (let i = 0; i < rows * cols; i++) {
result.push(matrix[r][c]);
visited.add(`${r},${c}`);
// Compute the next cell in the current direction.
let nr = r + dirs[d][0];
let nc = c + dirs[d][1];
// Turn clockwise if the next cell is off-grid or already seen.
if (
nr < 0 || nr >= rows || nc < 0 || nc >= cols ||
visited.has(`${nr},${nc}`)
) {
d = (d + 1) % 4; // rotate right → down → left → up → right
nr = r + dirs[d][0];
nc = c + dirs[d][1];
}
r = nr;
c = nc;
}
return result;
}
This produces the correct answer. But look at the cost. Every cell allocates a string key ("r,c") and does a Set insert plus a Set lookup on the turn check — that's hashing strings for all m × n cells. The visited set is O(m × n) extra memory just to re-derive a shape the matrix already has: a rectangle. The boundary method below tracks the same "what's left" information in four integers, with zero hashing and zero extra allocation. Same answer, a fraction of the work — and arguably easier to reason about, because the turn logic is replaced by four plain loops in a fixed order.
function matrixSpiralTraversal(matrix) {
const result = [];
// Empty grid OR a grid whose rows have no columns ([[]]) → nothing to visit.
if (matrix.length === 0 || matrix[0].length === 0) return result;
// Four walls fencing in the not-yet-visited rectangle, inclusive on both ends.
let top = 0;
let bottom = matrix.length - 1;
let left = 0;
let right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// 1. Top row, left → right.
for (let col = left; col <= right; col++) {
result.push(matrix[top][col]);
}
top++; // top row consumed; close the wall in from above
// 2. Right column, top → bottom.
for (let row = top; row <= bottom; row++) {
result.push(matrix[row][right]);
}
right--; // right column consumed; close in from the right
// 3. Bottom row, right → left — ONLY if a row still remains.
if (top <= bottom) {
for (let col = right; col >= left; col--) {
result.push(matrix[bottom][col]);
}
bottom--;
}
// 4. Left column, bottom → top — ONLY if a column still remains.
if (left <= right) {
for (let row = bottom; row >= top; row--) {
result.push(matrix[row][left]);
}
left++;
}
}
return result;
}
module.exports = { matrixSpiralTraversal };
The shift from the naive version is that the visited set is gone entirely. The four walls are the memory: any cell with top ≤ row ≤ bottom and left ≤ col ≤ right is unvisited; everything outside that box is done. Each pass walks one wall and then moves it, so the box strictly shrinks every iteration and the loop is guaranteed to terminate.
Two choices deserve a closer look. First, the boundaries are inclusive — bottom = matrix.length - 1, not matrix.length. That's why every loop uses <= / >= and why the bottom and left passes count down to left and up to top. Getting one of those comparisons wrong by a single step is the classic off-by-one that either drops a corner cell or reads matrix[bottom][-1].
Second, and this is the heart of the question, passes 3 and 4 are guarded by if (top <= bottom) and if (left <= right). The top and right passes (1 and 2) never need a guard because the while condition already proved top <= bottom && left <= right when the iteration began. But pass 1 increments top and pass 2 decrements right, so by the time we reach passes 3 and 4 the walls may have already crossed. Without those two guards, a matrix whose final leftover band is a single row or single column gets that line visited twice.
Let's trace the 3×4 rectangle from the prompt, because it's the smallest shape where the single-line guard actually fires:
matrix = [
[ 1, 2, 3, 4],
[10, 11, 12, 5],
[ 9, 8, 7, 6],
]
Initial walls: top=0, bottom=2, left=0, right=3. The while condition 0 <= 2 && 0 <= 3 holds, so we enter the first ring.
Ring 1
Pass 1 (top row 0, cols 0→3): push 1, 2, 3, 4 → top becomes 1
Pass 2 (right col 3, rows 1→2): push 5, 6 → right becomes 2
Pass 3 (guard 1<=2 ✓; row 2, cols 2→0): push 7, 8, 9 → bottom becomes 1
Pass 4 (guard 0<=2 ✓; col 0, rows 1→1): push 10 → left becomes 1
result so far: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After ring 1 the walls are top=1, bottom=1, left=1, right=2. The while check 1 <= 1 && 1 <= 2 still holds — there's an inner band left — so we go again.
Ring 2
Pass 1 (top row 1, cols 1→2): push 11, 12 → top becomes 2
Pass 2 (right col 2, rows 2→1): loop body never runs (2 > 1, empty range)
→ right becomes 1
Pass 3 (guard: top=2 <= bottom=1 ? NO → SKIP)
Pass 4 (guard: left=1 <= right=1 ? YES; col 1, rows 1→2: empty range)
result so far: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Here's the guard doing its job. After pass 1 of ring 2, top is 2 while bottom is still 1. Pass 3 asks top <= bottom — that's 2 <= 1, which is false, so the bottom pass is skipped. If it had run, it would have walked row = bottom = 1 from right back to left and re-pushed 12, 11 — a double-visit of the exact cells pass 1 just emitted. Pass 4's guard left <= right is 1 <= 1 (true), but its inner loop runs row from bottom=1 down to top=2, which is an empty range, so nothing is pushed. The walls have now crossed: the next while check 2 <= 1 is false, the loop exits, and we return the 12 values in spiral order.
The boundary shrink itself — all four walls stepping inward after one ring — looks like this:
top past bottom. If pass 3 isn't guarded by if (top <= bottom), it walks the same row backward and you get duplicates like [..., 11, 12, 12, 11]. The same happens on a single leftover column without if (left <= right) on pass 4. Both guards are mandatory, not optional polish.top before pass 2 reads the right column, pass 2 starts one row too low; if you decrement right before pass 1, pass 1 stops one column short. Walk-then-shrink, one wall at a time.bottom and right are the last valid index (length - 1), so the loops are <= / >=. A < where you needed <= silently drops the final cell of a pass (the corner gets skipped); going one past turns into an out-of-bounds read like matrix[row][-1], which is undefined. Pick inclusive and stay consistent across all four loops.matrixSpiralTraversal([]) has matrix.length === 0; matrixSpiralTraversal([[]]) has matrix[0].length === 0. Both must short-circuit to [] before you read matrix[0].length, because evaluating matrix[0].length on [] would read undefined.length and throw. Check matrix.length === 0 first so the || short-circuits.m > n), wide (m < n), and square (m === n) all run the identical loop. People sometimes write a separate branch for non-square inputs; you don't need one. The inclusive walls and the two guards already cover every shape, including 1 × n and m × 1.1 × n matrix: pass 1 emits the whole row, top becomes 1 > bottom (0). Pass 2's range is empty. Pass 3 is correctly skipped by its guard. A m × 1 matrix: pass 1 emits one cell, pass 2 emits the rest of the column, and the bottom/left guards keep the lone column from being re-read. These degenerate shapes are exactly why the guards exist.n × n). The inverse problem: given n, produce the matrix [[1,2,3],[8,9,4],[7,6,5]] where the numbers 1..n² spiral inward. Same four-wall loop, but instead of pushing matrix[r][c] you assign matrix[r][c] = counter++. Pre-allocate the grid, run the identical traversal, write instead of read.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.