Matrix and Grid Simulation
Rotate, spiral, zero out and simulate on a 2D grid using boundaries, direction arrays and in-place encodings.
- O(n²)Rotate an n by n grid in place
- O(1)Extra space with in-place markers
- (j, n-1-i)Where a clockwise turn sends cell (i, j)
Show what to say
“With a grid, I first write the coordinate rule: where does cell (i, j) go, or which cells share a property. Rotation is a transpose plus reversing each row, a spiral is four walls moving inward, and flags can live in the grid's first row and column. That is O(rows times columns) time and O(1) space.”
Trade-offs
- Copy vs encode in place: a copy is simple and safe; encoding saves a whole grid of space but needs a cleanup pass.
- Flag arrays vs first row and column as flags: arrays are easier and use O(rows + columns); in-place needs two booleans.
- Transpose and reverse vs ring rotation: both are O(n²) time and O(1) space; the two-step version is easier to get right.
Bring it up before they ask
Try 1 by n, n by 1 and 1 by 1 grids, and never write new Array(rows).fill(new Array(cols)): every row would be the same array.
A grid problem is usually index arithmetic in disguise: rotate with two swaps, walk a spiral with four moving walls, mark rows inside the grid itself. Get the coordinates right and most of these are O(rows × columns) time with O(1) extra space.
When to reach for it
Tap the signals you can see in the problem.
Rememberrotate, flip or transposespiraldiagonalsin placeat the same timesorted
- Rotate / transpose
Key moveswap
rotate image(i, j)with(j, i), reverse rows - Layers and boundaries
Key movefour walls that move inward
spiral matrix - Marker row and column
Key movestore flags in the grid itself
set matrix zeroes - Simulation
Key moveencode old and new state in one cell
game of life - Sorted grid / diagonals
Key moveposition to (row, column), or
search a 2D matrixdiagonal traverser + c
The core idea: a grid is a set of coordinates
A grid is an array of arrays, and each cell has a row and column you can compute with.
Read more · 74 words
A clockwise turn sends (i, j) to (j, n - 1 - i). A diagonal is every cell with the same r + c. A position in a sorted grid read row by row is r = Math.floor(p / cols) and c = p % cols.
Rotation is the clearest example. Do it as two easy moves, each done in place:
Before coding, write the formula for where one cell goes, and check it on a 2 × 2 grid.
Rotate 90 degrees clockwise: transpose, then reverse every row
- n
- 3
Rotating a grid in place sounds hard, so split it in two easy moves. First transpose: swap every cell above the diagonal with its mirror across it.
function rotate(m) {
const n = m.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
[m[i][j], m[j][i]] = [m[j][i], m[i][j]];
}
}
for (const row of m) row.reverse();
}All steps as text
- Rotating a grid in place sounds hard, so split it in two easy moves. First transpose: swap every cell above the diagonal with its mirror across it.
- Swap m[0][1] = 2 with m[1][0] = 4. Only cells with j > i are visited, so each pair is swapped once and not undone.
- Swap m[0][2] = 3 with m[2][0] = 7. Only cells with j > i are visited, so each pair is swapped once and not undone.
- Swap m[1][2] = 6 with m[2][1] = 8. Only cells with j > i are visited, so each pair is swapped once and not undone.
- The transpose is done: rows have become columns. Now reverse each row, which mirrors the grid left to right.
- Row 0 reversed. After this, the old row 0 reads top to bottom down column 2.
- Row 1 reversed. After this, the old row 1 reads top to bottom down column 1.
- Row 2 reversed. After this, the old row 2 reads top to bottom down column 0.
- Transpose plus reverse rows equals a clockwise quarter turn, using no second grid: O(1) extra space.
Play with it
Step through the rotation, then switch to Spiral and watch the four walls move in.
Read more · 140 words
Try the 2×5 shape: a single row is where spiral code breaks.
Then work in this order.
Write the coordinate rule
Where does the cell at
(i, j)go, or which cells share a property? Test it on a 2 × 2 or 3 × 3 grid.Pick the loop bounds
Is it all cells, only above the diagonal (
j > i), or the inner cells1..n-1? Off-by-one errors live here.Decide where the new state goes
In place needs a trick (swaps, flags in the grid, an encoding). A copy is fine if the statement allows it.
Never mix old and new
If every cell must see the old grid, mark changes without applying them until the scan is done.
Try the thin shapes
Run 1 × n, n × 1 and 1 × 1 grids. Spiral and zeroes bugs show up there.
Templates
1. Rotate and transpose
Transpose then reverse each row rotates a square grid clockwise in place.
Read more · 42 words
A rectangle cannot be transposed in place (the shape changes), so build a new grid.
This is Rotate ImageMedium, and Transpose MatrixEasy is the same swap on a new grid. For the other direction, reverse the rows first, or transpose then reverse the columns.
// Template 1: rotate by combining two simple moves. Transpose swaps (i, j) with (j, i); reversing every row
// then mirrors left to right. Transpose + reverse rows = 90 degrees clockwise, in place, O(1) extra space.
export function transpose(m) {
// A new grid, so it also works for rectangles: rows x cols becomes cols x rows.
const out = Array.from({ length: m[0].length }, () => new Array(m.length)); // never fill() with one shared row
for (let i = 0; i < m.length; i++) for (let j = 0; j < m[0].length; j++) out[j][i] = m[i][j];
return out;
}
export function rotateClockwise(m) {
const n = m.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) [m[i][j], m[j][i]] = [m[j][i], m[i][j]]; // j > i: swap each pair once
}
for (const row of m) row.reverse();
}
export function rotateCounterClockwise(m) {
for (const row of m) row.reverse(); // reverse first, then transpose, for the other direction
const n = m.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) [m[i][j], m[j][i]] = [m[j][i], m[i][j]];
}
}2. Layers and boundaries
A spiral is a series of laps round a shrinking rectangle.
Read more · 52 words
Keep top, bottom, left and right, read a side, and move that wall inward.
This reads Spiral MatrixMedium. The two if checks stop a leftover row or column being read twice. The file also fills a grid with 1 to n² using a direction array and a turn when blocked ("Spiral Matrix II").
Spiral order: four walls that move inward
- top
- 0
- bottom
- 2
- left
- 0
- right
- 3
Four walls frame the unread part of the grid: top 0, bottom 2, left 0, right 3. We read its border clockwise, then shrink the frame.
function spiralOrder(m) {
const out = [];
let top = 0, bottom = m.length - 1;
let left = 0, right = m[0].length - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) out.push(m[top][c]);
top++;
for (let r = top; r <= bottom; r++) out.push(m[r][right]);
right--;
if (top <= bottom) {
for (let c = right; c >= left; c--) out.push(m[bottom][c]);
bottom--;
}
if (left <= right) {
for (let r = bottom; r >= top; r--) out.push(m[r][left]);
left++;
}
}
return out;
}All steps as text
- Four walls frame the unread part of the grid: top 0, bottom 2, left 0, right 3. We read its border clockwise, then shrink the frame.
- Read the top row, left to right. That row is finished, so the top wall moves down: top = 1.
- Read the right column, top to bottom. Now the right wall moves in: right = 2.
- Read the bottom row, right to left. The bottom wall moves up: bottom = 1.
- Read the left column, bottom to top. The left wall moves in: left = 1.
- Read the top row, left to right. That row is finished, so the top wall moves down: top = 2.
- No rows are left under the top wall, so the right column adds nothing.
- top has passed bottom: there is no separate bottom row. Without this check a single leftover row would be read twice.
- No rows are left, so the left column adds nothing.
- The walls have crossed, so every cell was read exactly once: 12 cells, O(rows × columns).
// Template 2: layers and boundaries. Four walls frame the unread rectangle; after each side is read,
// that wall moves inward. The two guards stop a leftover single row or column being read twice.
export function spiralOrder(m) {
const out = [];
let top = 0;
let bottom = m.length - 1;
let left = 0;
let right = m[0].length - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) out.push(m[top][c]);
top++;
for (let r = top; r <= bottom; r++) out.push(m[r][right]);
right--;
if (top <= bottom) {
for (let c = right; c >= left; c--) out.push(m[bottom][c]);
bottom--;
}
if (left <= right) {
for (let r = bottom; r >= top; r--) out.push(m[r][left]);
left++;
}
}
return out;
}
// The same spiral written the other way round: a direction array and "turn when blocked", to fill 1..n*n.
export function spiralFill(n) {
const grid = Array.from({ length: n }, () => new Array(n).fill(0));
const dr = [0, 1, 0, -1]; // right, down, left, up
const dc = [1, 0, -1, 0];
let r = 0;
let c = 0;
let d = 0;
for (let k = 1; k <= n * n; k++) {
grid[r][c] = k;
const nr = r + dr[d];
const nc = c + dc[d];
if (nr < 0 || nr >= n || nc < 0 || nc >= n || grid[nr][nc] !== 0) d = (d + 1) % 4; // blocked: turn right
r += dr[d];
c += dc[d];
}
return grid;
}More templates
3. Marker row and column
Zeroing a row the moment you see a 0 is wrong: new zeroes look like old ones.
Read more · 39 words
Decide first, change later, and keep the flags in the grid's own first row and first column.
This solves Set Matrix ZeroesMedium. The first row and column hold flags and data, so two booleans remember if they started with a zero.
// Template 3: marker row and column. Record "this row/column must be cleared" inside the grid itself (its first
// row and first column), instead of in extra arrays. Decide first, change later: never zero while scanning.
export function setZeroes(m) {
const rows = m.length;
const cols = m[0].length;
let firstRowZero = false; // the markers live in row 0 and column 0, so remember if they held a zero
let firstColZero = false;
for (let j = 0; j < cols; j++) if (m[0][j] === 0) firstRowZero = true;
for (let i = 0; i < rows; i++) if (m[i][0] === 0) firstColZero = true;
for (let i = 1; i < rows; i++) {
for (let j = 1; j < cols; j++) {
if (m[i][j] === 0) {
m[i][0] = 0; // mark row i
m[0][j] = 0; // mark column j
}
}
}
for (let i = 1; i < rows; i++) {
for (let j = 1; j < cols; j++) {
if (m[i][0] === 0 || m[0][j] === 0) m[i][j] = 0;
}
}
if (firstRowZero) for (let j = 0; j < cols; j++) m[0][j] = 0; // only now is it safe to clear the markers
if (firstColZero) for (let i = 0; i < rows; i++) m[i][0] = 0;
}4. Simulation with state encoding
When all cells change at once, a decision must read the old grid.
Read more · 51 words
Store old and new state in one number: 2 means alive now and dead next, 3 means dead now and alive next. Count neighbours with a direction array of eight steps.
This is Game of LifeMedium. A flood fill or a rotting spread uses the same direction array; see Graph BFS and DFS.
Game of Life in place: store old and new state in one cell
- generation
- "old"
Every cell must be judged by the old generation. We cannot overwrite a cell with its new state yet, or its neighbours would see a mix.
function gameOfLife(b) {
// 0 dead, 1 alive, 2 alive -> dead, 3 dead -> alive
for (let r = 0; r < b.length; r++) {
for (let c = 0; c < b[0].length; c++) {
const live = aliveNeighbours(b, r, c);
if (b[r][c] === 1 && (live < 2 || live > 3)) b[r][c] = 2;
else if (b[r][c] === 0 && live === 3) b[r][c] = 3;
}
}
for (const row of b)
for (let c = 0; c < row.length; c++) row[c] = row[c] === 1 || row[c] === 3 ? 1 : 0;
}All steps as text
- Every cell must be judged by the old generation. We cannot overwrite a cell with its new state yet, or its neighbours would see a mix.
- Cell (0, 0) is dead with 2 live neighbours: no birth, so it keeps its value.
- Cell (0, 1) is alive with 1 live neighbours: too lonely. Mark it 2: alive now, dead next. Neighbours still count it as alive.
- Cell (0, 2) is dead with 2 live neighbours: no birth, so it keeps its value.
- Cell (1, 0) is dead with exactly 3 live neighbours, so it is born. Mark it 3: dead now, alive next. Neighbours still count it as dead.
- Cell (1, 1) is alive with 2 live neighbours: it survives, so it keeps its value.
- Cell (1, 2) is dead with exactly 3 live neighbours, so it is born. Mark it 3: dead now, alive next. Neighbours still count it as dead.
- Cell (2, 0) is dead with 2 live neighbours: no birth, so it keeps its value.
- Cell (2, 1) is alive with 1 live neighbours: too lonely. Mark it 2: alive now, dead next. Neighbours still count it as alive.
- Cell (2, 2) is dead with 2 live neighbours: no birth, so it keeps its value.
- One last pass keeps only the new state: 1 and 3 become 1, 0 and 2 become 0. No second board was needed.
// Template 4: simulation with state encoding. All cells change at once, so decisions must use the OLD board.
// Encode old and new state in one number: 0 dead, 1 alive, 2 alive -> dead, 3 dead -> alive.
// Eight directions as (row, column) steps: the direction-array idiom for every grid walk.
const NEIGHBOURS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
function aliveNeighbours(board, r, c) {
let live = 0;
for (const [dr, dc] of NEIGHBOURS) {
const nr = r + dr;
const nc = c + dc;
if (nr < 0 || nr >= board.length || nc < 0 || nc >= board[0].length) continue;
if (board[nr][nc] === 1 || board[nr][nc] === 2) live++; // 2 was alive in the old generation
}
return live;
}
export function gameOfLife(board) {
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[0].length; c++) {
const live = aliveNeighbours(board, r, c);
if (board[r][c] === 1 && (live < 2 || live > 3)) board[r][c] = 2; // dies
else if (board[r][c] === 0 && live === 3) board[r][c] = 3; // is born
}
}
for (const row of board) {
for (let c = 0; c < row.length; c++) row[c] = row[c] === 1 || row[c] === 3 ? 1 : 0; // keep the new state
}
}
// The simple alternative: write into a copy, read from the original. O(rows * columns) extra space.
export function nextGeneration(board) {
const next = board.map((row) => row.slice());
for (let r = 0; r < board.length; r++) {
for (let c = 0; c < board[0].length; c++) {
const live = aliveNeighbours(board, r, c);
next[r][c] = board[r][c] === 1 ? (live === 2 || live === 3 ? 1 : 0) : live === 3 ? 1 : 0;
}
}
return next;
}5. Binary search and diagonals
If each row starts above the previous row's end, the grid is one sorted list.
Read more · 64 words
Search positions and convert with / and %. If only rows and columns are sorted, walk a staircase from the top-right corner. Diagonals group by r + c.
These solve Search a 2D MatrixMedium and Diagonal TraverseMedium. For Valid SudokuMedium use one set per row, column and box, with box index Math.floor(r / 3) * 3 + Math.floor(c / 3). Maximal RectangleHard turns each row into a histogram, then uses a monotonic stack.
// Template 5a: binary search on a matrix. If each row starts above the previous row's end, the grid read row by
// row is one sorted list: search positions 0..rows*cols-1 and convert a position to (row, column).
export function searchFlat(m, target) {
const cols = m[0].length;
let lo = 0;
let hi = m.length * cols - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const value = m[Math.floor(mid / cols)][mid % cols];
if (value === target) return true;
if (value < target) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
// If rows and columns are each sorted but rows do not chain, walk a staircase from the top-right corner:
// too big means nothing below-left of here can match in this column, so go left; too small, go down.
export function searchStaircase(m, target) {
let r = 0;
let c = m[0].length - 1;
while (r < m.length && c >= 0) {
if (m[r][c] === target) return true;
if (m[r][c] > target) c--;
else r++;
}
return false;
}Try it
// Edit the grids and press Run. These are the templates above.
const grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(transpose([[1, 2, 3], [4, 5, 6]])); // [ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]
const r = grid.map((row) => [...row]);
rotateClockwise(r);
console.log(r); // [ [ 7, 4, 1 ], [ 8, 5, 2 ], [ 9, 6, 3 ] ]
console.log(spiralOrder(grid).join(' ')); // 1 2 3 6 9 8 7 4 5
console.log(spiralFill(3)); // [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
console.log(diagonalOrder(grid).join(' ')); // 1 2 4 7 5 3 6 8 9
const z = [[1, 1, 1], [1, 0, 1], [1, 1, 1]];
setZeroes(z);
console.log(z); // [ [ 1, 0, 1 ], [ 0, 0, 0 ], [ 1, 0, 1 ] ]
const life = [[0, 1, 0], [0, 1, 0], [0, 1, 0]];
gameOfLife(life);
console.log(life); // [ [ 0, 0, 0 ], [ 1, 1, 1 ], [ 0, 0, 0 ] ]
console.log(searchFlat([[1, 3, 5, 7], [10, 11, 16, 20]], 11), searchStaircase([[1, 4, 7], [2, 5, 8], [3, 6, 9]], 6)); // true trueComplexity
Read more · 48 words
| Variant | Time | Extra space |
|---|---|---|
| Rotate in place | O(n²) | O(1) |
| Spiral, diagonals | O(rows × columns) | O(1) beyond the output |
| Marker row and column | O(rows × columns) | O(1) |
| Game of life, encoded | O(rows × columns) | O(1) |
| Sorted grid search | O(log(rows × columns)) | O(1) |
JavaScript you'll use
JavaScript refresher: Arrays: create, read, update Gotchas and debugging Destructuring, spread and restRead more · 72 words
The classic trap is new Array(rows).fill(new Array(cols).fill(0)): every row is the same array.
const grid = Array.from({ length: rows }, () => new Array(cols).fill(0)); // each row separate
[m[i][j], m[j][i]] = [m[j][i], m[i][j]]; // swap two cells
for (const row of m) row.reverse(); // reverse in place
const DIRS = [[0, 1], [1, 0], [0, -1], [-1, 0]]; // right, down, left, up
const inside = (r, c) => r >= 0 && r < rows && c >= 0 && c < cols;Interview questions
How do you rotate a non-square grid?
You cannot do it in place, because the shape changes from m × n to n × m. Allocate the new grid and copy each (i, j) to (j, m - 1 - i).
How would you rotate 90 degrees without transposing?
Rotate in rings: for each layer and offset, cycle four cells (top, right, bottom, left). Same O(n²) time and O(1) space, but the indices are easier to get wrong.
Game of Life on an unbounded board?
Store only the live cells in a set. Count neighbours for live cells and their neighbours, so cost follows the live cells, not the board.
What if rows and columns are sorted, but rows do not chain?
Start top-right: too big, go left; too small, go down. That is O(rows + columns); a binary search per row costs O(rows × log columns).
How would you store a huge sparse matrix?
Keep a map from (row, column) to value, or compressed rows. Memory scales with the non-zero cells, and transposing becomes swapping the key pair.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Transpose MatrixFlip a rectangular grid over its main diagonal so that rows become columns.Easy
- Rotate ImageTurn a square grid 90 degrees clockwise without allocating a second grid.Medium
- Spiral MatrixRead every value of a rectangular grid in clockwise spiral order, starting from the top-left corner.Medium
- Set Matrix ZeroesIf a cell holds 0, set its whole row and whole column to 0, doing the update inside the grid itself.Medium
- Diagonal TraverseRead a grid along its anti-diagonals, zig-zagging up-right then down-left, starting at the top-left cell.Medium
- Search a 2D MatrixFind a value in a grid whose rows are sorted and where each row starts above the end of the previous row.Medium
- Valid SudokuCheck that the filled cells of a 9 x 9 puzzle break no row, column or 3 x 3 box rule.Medium
- Game of LifeAdvance a grid of live and dead cells by one generation, where every cell changes at the same moment.Medium
- Maximal RectangleFind the area of the largest rectangle made only of 1s inside a grid of 0s and 1s.Hard
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.