Pattern 24 of 24 · Arrays & strings · 35 min

Matrix and Grid Simulation

Rotate, spiral, zero out and simulate on a 2D grid using boundaries, direction arrays and in-place encodings.

Spot it:rotate or transpose a matrixwalk in a spiral or along diagonalsupdate the grid in placeall cells change at the same timeJavaScript you'll use: Arrays: create, read, update Gotchas and debugging Destructuring, spread and rest
Say it in 30 seconds
  • 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.

0 of 0 sections ticked
30-second answer

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

Spot it in the problem0 of 4

Tap the signals you can see in the problem.

Rememberrotate, flip or transposespiraldiagonalsin placeat the same timesorted

Pick your variant
  • Rotate / transpose

    Key moveswap (i, j) with (j, i), reverse rows

    rotate image
  • 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 r + c

    search a 2D matrixdiagonal traverse

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

matrix (3×3)
123
456
789
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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. The transpose is done: rows have become columns. Now reverse each row, which mirrors the grid left to right.
  6. Row 0 reversed. After this, the old row 0 reads top to bottom down column 2.
  7. Row 1 reversed. After this, the old row 1 reads top to bottom down column 1.
  8. Row 2 reversed. After this, the old row 2 reads top to bottom down column 0.
  9. 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.

  1. 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.

  2. Pick the loop bounds

    Is it all cells, only above the diagonal (j > i), or the inner cells 1..n-1? Off-by-one errors live here.

  3. 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.

  4. Never mix old and new

    If every cell must see the old grid, mark changes without applying them until the scan is done.

  5. Try the thin shapes

    Run 1 × n, n × 1 and 1 × 1 grids. Spiral and zeroes bugs show up there.

Interactive

Matrix lab

Rotate a grid in place, or read it in a spiral with four moving walls.

Original123456789step 0 of 7

Start. Step 1 mirrors the grid across its main diagonal (transpose); step 2 reverses each row.

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

matrix (3×4)
1234
5678
9101112
output (0)
empty
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
  1. 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.
  2. Read the top row, left to right. That row is finished, so the top wall moves down: top = 1.
  3. Read the right column, top to bottom. Now the right wall moves in: right = 2.
  4. Read the bottom row, right to left. The bottom wall moves up: bottom = 1.
  5. Read the left column, bottom to top. The left wall moves in: left = 1.
  6. Read the top row, left to right. That row is finished, so the top wall moves down: top = 2.
  7. No rows are left under the top wall, so the right column adds nothing.
  8. top has passed bottom: there is no separate bottom row. Without this check a single leftover row would be read twice.
  9. No rows are left, so the left column adds nothing.
  10. 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

board (3×3)
010
010
010
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
  1. 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.
  2. Cell (0, 0) is dead with 2 live neighbours: no birth, so it keeps its value.
  3. Cell (0, 1) is alive with 1 live neighbours: too lonely. Mark it 2: alive now, dead next. Neighbours still count it as alive.
  4. Cell (0, 2) is dead with 2 live neighbours: no birth, so it keeps its value.
  5. 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.
  6. Cell (1, 1) is alive with 2 live neighbours: it survives, so it keeps its value.
  7. 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.
  8. Cell (2, 0) is dead with 2 live neighbours: no birth, so it keeps its value.
  9. Cell (2, 1) is alive with 1 live neighbours: too lonely. Mark it 2: alive now, dead next. Neighbours still count it as alive.
  10. Cell (2, 2) is dead with 2 live neighbours: no birth, so it keeps its value.
  11. 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

try-it.js
// 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 true

Complexity

Read more · 48 words
VariantTimeExtra space
Rotate in placeO(n²)O(1)
Spiral, diagonalsO(rows × columns)O(1) beyond the output
Marker row and columnO(rows × columns)O(1)
Game of life, encodedO(rows × columns)O(1)
Sorted grid searchO(log(rows × columns))O(1)

JavaScript you'll use

JavaScript refresher: Arrays: create, read, update Gotchas and debugging Destructuring, spread and rest
Read 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 3

Score 2 out of 3 or better to mark this page complete.

1. To rotate a square grid 90 degrees clockwise in place, which two moves do the job?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.