GUIDE · Patterns

Matrices (2-D Grids)

grid[r][c] indexing, traversal orders, in-place transforms, grid-as-graph search.

lesson16 sectionsruns in browser37 asserts243 lines

At a glance

WHAT
grid[r][c] indexing, traversal orders, in-place transforms, grid-as-graph search.
TELL
"m x n grid", "rotate image", "spiral", "islands", "flood fill", "neighbors", "cells", "minutes until all ...", "search a sorted matrix".
SAY
row-majordirections arrayin-bounds checktransposereverse rowsmulti-source BFSstaircase searchsentinel / marker values4-directional vs 8-directional neighbors
BIG-O
O(mn) for any full traversal; BFS/DFS on grid O(mn); staircase search O(m + n).

The idea#

Picture a sheet of graph paper. Each square has a row and a column, grid[r][c]. Reading left to right, then down to the next row, is row-major order. That is the order the squares sit in memory, and it is the friendly way to walk them. Column-major walks down first.

A square's neighbors are the squares that share a side: down, up, right, left. You keep those four steps in one directions array and loop it, instead of writing four copies of the same bounds check. The check is the edge of the paper. A row or a column that falls off the sheet is not a square. Eight-direction neighbors include the corners, which is what a Game of Life cell counts.

Some questions treat the paper as a picture you turn or a map you search. Turning a square photo 90 degrees clockwise is a transpose, swapping grid[r][c] with grid[c][r], then reversing each row. Searching the paper for land is a graph search where each square is a node and each shared side is an edge. When you visit a land square you sink it, so the next walk does not count that island again.

Why it matters#

You reach for this when the problem says an m by n grid, rotate an image, spiral, islands, flood fill, neighbors, cells, minutes until everything has spread, or search a sorted matrix. The bug is almost always an off-the-paper index, or a visit you forgot to mark, so the same island gets counted twice.

Any full walk of the paper is O(mn). Breadth-first and depth-first search over the squares are O(mn) too. Staircase search, on a matrix whose rows and columns are each sorted, is O(m + n).

A small example#

JS
function countIslands(grid) {  const rows = grid.length;  const cols = grid[0].length;  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];  function sink(r, c) {    if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] !== 1) return;    grid[r][c] = 0;    for (const [dr, dc] of dirs) sink(r + dr, c + dc);  }  let islands = 0;  for (let r = 0; r < rows; r++) {    for (let c = 0; c < cols; c++) {      if (grid[r][c] === 1) {        islands += 1;        sink(r, c);      }    }  }  return islands;} const n = countIslands([[1, 1, 0], [0, 1, 0], [0, 0, 1]]);

Two clumps of 1s share sides inside themselves and not with each other. n is 2. Sinking a square to 0 is the mark that keeps the walk from coming back.

What the code below is doing#

The sheet opens with the four-direction array and an in-bounds helper, then row-major and column-major walks. Transpose writes out[c][r] = grid[r][c], and it works for a rectangle. Rotate 90 clockwise in place is that transpose plus reversing each row. Counterclockwise is the transpose plus reversing each column. Reversing the rows and then transposing is the other route. Spiral order keeps four shrinking boundaries, top, bottom, left, and right, and it checks the bounds again before the bottom row and the left column so a single remaining row or column is not walked twice.

Set-matrix-zeroes uses row 0 and column 0 as the marker arrays, with a separate flag for column 0, so the extra space stays constant. Search in a fully sorted matrix binary-searches the flat index, O(log(mn)). Search when each row and each column is sorted starts at the top-right corner and walks like a staircase: too big moves left, too small moves down, O(m + n).

Number of islands, flood fill, and max island area are the graph searches. Islands and flood fill sink or recolor as they go. Flood fill bails out when the new color is already the old color. Max area returns 1 plus the sum of the neighbor calls. Rotting oranges is multi-source breadth-first search: every rotten orange is queued before the clock starts, each layer of the queue is one minute, and fresh oranges left at the end mean the answer fails. Diagonal traversal groups squares that share r + c and flips direction with the parity of that sum. Game of Life counts eight neighbors and stores the next state in an extra bit so the board stays in place. Pascal's triangle sets the edges to 1 and fills each inner cell from the two parents above it. Word search is grid depth-first search with backtracking: mark the cell, try four directions, restore it, at O(mn · 3^L).

When you talk about it, say this: row and column, a directions array, an in-bounds check, and whether you are turning the paper or searching it.

Everything below is the demonstration. Same idea, written as code you can run.

JSalgorithms/matrices.js · setup
'use strict';const assert = require('node:assert/strict');
01

Directions idiom + in-bounds helper#

iterate neighbors with one loop

JS
const DIRS4 = [[1, 0], [-1, 0], [0, 1], [0, -1]];const DIRS8 = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]];const inBounds = (g, r, c) => r >= 0 && c >= 0 && r < g.length && c < g[0].length;const neighbors = (g, r, c, dirs = DIRS4) => dirs.map(([dr, dc]) => [r + dr, c + dc]).filter(([nr, nc]) => inBounds(g, nr, nc));const g3 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
Verified by 3 asserts
JS
assert.deepEqual(neighbors(g3, 0, 0), [[1, 0], [0, 1]]);assert.equal(neighbors(g3, 1, 1, DIRS8).length, 8);assert.equal(inBounds(g3, 3, 0), false);
02

Traverse row-major / col-major#

row-major is cache-friendly and matches memory layout

JS
const rowMajor = (g) => g.flat();const colMajor = (g) => { const out = []; for (let c = 0; c < g[0].length; c++) for (let r = 0; r < g.length; r++) out.push(g[r][c]); return out; };// create m x n: Array.from({ length: m }, () => new Array(n).fill(0))   -- NEVER new Array(m).fill([]) (shared row!)
Verified by 2 asserts
JS
assert.deepEqual(rowMajor(g3), [1, 2, 3, 4, 5, 6, 7, 8, 9]);assert.deepEqual(colMajor(g3), [1, 4, 7, 2, 5, 8, 3, 6, 9]);
03

Transpose#

out[c][r] = g[r][c]; works for non-square

O(mn)
JS
const transpose = (g) => g[0].map((_, c) => g.map((row) => row[c]));
Verified by 2 asserts
JS
assert.deepEqual(transpose([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]]);assert.deepEqual(transpose([[1]]), [[1]]);
04

Rotate 90 clockwise in place#

transpose then reverse each row. CCW: transpose then reverse each COLUMN (or reverse rows first)

O(n^2) / O(1)
JS
function rotateCW(m) {  const n = m.length;  for (let r = 0; r < n; r++) for (let c = r + 1; c < n; c++) [m[r][c], m[c][r]] = [m[c][r], m[r][c]]; // transpose  for (const row of m) row.reverse();  return m;}function rotateCCW(m) {  for (const row of m) row.reverse(); // reverse rows first ...  const n = m.length;  for (let r = 0; r < n; r++) for (let c = r + 1; c < n; c++) [m[r][c], m[c][r]] = [m[c][r], m[r][c]]; // ... then transpose  return m;}
Verified by 3 asserts
JS
assert.deepEqual(rotateCW([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), [[7, 4, 1], [8, 5, 2], [9, 6, 3]]);assert.deepEqual(rotateCCW([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), [[3, 6, 9], [2, 5, 8], [1, 4, 7]]);assert.deepEqual(rotateCW([[1]]), [[1]]);
05

Spiral order#

four shrinking boundaries top/bottom/left/right; check bounds before bottom row and left col

O(mn)
JS
function spiralOrder(m) {  const out = [];  if (m.length === 0) return out;  let top = 0, bottom = m.length - 1, 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;}
Verified by 3 asserts
JS
assert.deepEqual(spiralOrder([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), [1, 2, 3, 6, 9, 8, 7, 4, 5]);assert.deepEqual(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.deepEqual(spiralOrder([[1], [2], [3]]), [1, 2, 3]);
06

Set matrix zeroes, O(1) space#

use row 0 / col 0 as the marker arrays; separate flag for col 0

O(mn) / O(1)
JS
function setZeroes(m) {  const R = m.length, C = m[0].length;  let col0 = false;  for (let r = 0; r < R; r++) {    if (m[r][0] === 0) col0 = true;    for (let c = 1; c < C; c++) if (m[r][c] === 0) { m[r][0] = 0; m[0][c] = 0; }  }  for (let r = R - 1; r >= 0; r--) { // bottom-up so markers in row 0 survive until used    for (let c = C - 1; c >= 1; c--) if (m[r][0] === 0 || m[0][c] === 0) m[r][c] = 0;    if (col0) m[r][0] = 0;  }  return m;}
Verified by 3 asserts
JS
assert.deepEqual(setZeroes([[1, 1, 1], [1, 0, 1], [1, 1, 1]]), [[1, 0, 1], [0, 0, 0], [1, 0, 1]]);assert.deepEqual(setZeroes([[0, 1, 2, 0], [3, 4, 5, 2], [1, 3, 1, 5]]), [[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]]);assert.deepEqual(setZeroes([[1]]), [[1]]);
07

Search 2D matrix I (fully sorted as flat)#

binary search on index 0..mn-1

O(log mn)
JS
function searchMatrixI(m, t) {  const R = m.length, C = m[0].length;  let lo = 0, hi = R * C - 1;  while (lo <= hi) {    const mid = (lo + hi) >> 1, v = m[(mid / C) | 0][mid % C];    if (v === t) return true;    if (v < t) lo = mid + 1; else hi = mid - 1;  }  return false;}
08

Search 2D matrix II (rows and cols sorted), staircase#

start top-right; too big -> left, too small -> down

O(m + n)
JS
function searchMatrixII(m, t) {  let r = 0, c = m[0].length - 1;  while (r < m.length && c >= 0) {    if (m[r][c] === t) return true;    if (m[r][c] > t) c--; else r++;  }  return false;}
Verified by 4 asserts
JS
assert.equal(searchMatrixI([[1, 3, 5], [7, 9, 11]], 9), true);assert.equal(searchMatrixI([[1, 3, 5], [7, 9, 11]], 4), false);assert.equal(searchMatrixII([[1, 4, 7], [2, 5, 8], [3, 6, 9]], 5), true);assert.equal(searchMatrixII([[1, 4, 7], [2, 5, 8], [3, 6, 9]], 10), false);
09

Number of islands (DFS)#

sink visited land

O(mn)
JS
function numIslands(grid) {  let count = 0;  const sink = (r, c) => {    if (!inBounds(grid, r, c) || grid[r][c] !== '1') return;    grid[r][c] = '0';    for (const [dr, dc] of DIRS4) sink(r + dr, c + dc);  };  for (let r = 0; r < grid.length; r++) for (let c = 0; c < grid[0].length; c++) if (grid[r][c] === '1') { count++; sink(r, c); }  return count;}
Verified by 2 asserts
JS
assert.equal(numIslands([['1', '1', '0', '0'], ['0', '1', '0', '1'], ['0', '0', '0', '1']]), 2);assert.equal(numIslands([['0']]), 0);
10

Flood fill#

DFS from (sr, sc) over cells equal to the original color; guard same-color no-op

O(mn)
JS
function floodFill(img, sr, sc, color) {  const orig = img[sr][sc];  if (orig === color) return img;  const fill = (r, c) => {    if (!inBounds(img, r, c) || img[r][c] !== orig) return;    img[r][c] = color;    for (const [dr, dc] of DIRS4) fill(r + dr, c + dc);  };  fill(sr, sc);  return img;}
Verified by 2 asserts
JS
assert.deepEqual(floodFill([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 2), [[2, 2, 2], [2, 2, 0], [2, 0, 1]]);assert.deepEqual(floodFill([[0, 0], [0, 0]], 0, 0, 0), [[0, 0], [0, 0]]); // same color: no infinite loop
11

Max area of island#

DFS returns 1 + sum of neighbors

O(mn)
JS
function maxAreaOfIsland(grid) {  const area = (r, c) => {    if (!inBounds(grid, r, c) || grid[r][c] !== 1) return 0;    grid[r][c] = 0;    return 1 + DIRS4.reduce((s, [dr, dc]) => s + area(r + dr, c + dc), 0);  };  let best = 0;  for (let r = 0; r < grid.length; r++) for (let c = 0; c < grid[0].length; c++) best = Math.max(best, area(r, c));  return best;}
Verified by 2 asserts
JS
assert.equal(maxAreaOfIsland([[0, 1, 1, 0], [0, 1, 0, 0], [1, 0, 0, 1]]), 3);assert.equal(maxAreaOfIsland([[0, 0]]), 0);
12

Rotting oranges (multi-source BFS)#

enqueue ALL rotten first; BFS by levels; answer = levels; fail if fresh remains

O(mn)
JS
function orangesRotting(grid) {  const q = [];  let fresh = 0;  for (let r = 0; r < grid.length; r++) for (let c = 0; c < grid[0].length; c++) {    if (grid[r][c] === 2) q.push([r, c]);    else if (grid[r][c] === 1) fresh++;  }  let minutes = 0;  for (let h = 0; h < q.length && fresh > 0; minutes++) {    const levelEnd = q.length; // process one level    for (; h < levelEnd; h++) {      const [r, c] = q[h];      for (const [dr, dc] of DIRS4) {        const nr = r + dr, nc = c + dc;        if (inBounds(grid, nr, nc) && grid[nr][nc] === 1) { grid[nr][nc] = 2; fresh--; q.push([nr, nc]); }      }    }  }  return fresh === 0 ? minutes : -1;}
Verified by 3 asserts
JS
assert.equal(orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]]), 4);assert.equal(orangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]]), -1);assert.equal(orangesRotting([[0, 2]]), 0);
13

Diagonal traversal (zigzag)#

cells on the same anti-diagonal share r + c; alternate direction by parity

O(mn)
JS
function findDiagonalOrder(m) {  const R = m.length, C = m[0].length, out = [];  for (let d = 0; d < R + C - 1; d++) {    const cells = [];    for (let r = Math.max(0, d - C + 1); r <= Math.min(R - 1, d); r++) cells.push(m[r][d - r]);    out.push(...(d % 2 === 0 ? cells.reverse() : cells)); // even diagonals go up-right  }  return out;}
Verified by 2 asserts
JS
assert.deepEqual(findDiagonalOrder([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), [1, 2, 4, 7, 5, 3, 6, 8, 9]);assert.deepEqual(findDiagonalOrder([[1, 2], [3, 4]]), [1, 2, 3, 4]);
14

Game of life neighbor counting#

8-dir count; encode next state in extra bits (bit 1) to stay in place

O(mn) / O(1)
JS
function gameOfLife(b) {  const R = b.length, C = b[0].length;  for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {    let live = 0;    for (const [dr, dc] of DIRS8) if (inBounds(b, r + dr, c + dc) && (b[r + dr][c + dc] & 1)) live++;    if ((b[r][c] & 1) && (live === 2 || live === 3)) b[r][c] |= 2; // survives    if (!(b[r][c] & 1) && live === 3) b[r][c] |= 2; // born  }  for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) b[r][c] >>= 1; // shift next state into place  return b;}
Verified by 2 asserts
JS
assert.deepEqual(gameOfLife([[0, 1, 0], [0, 0, 1], [1, 1, 1], [0, 0, 0]]), [[0, 0, 0], [1, 0, 1], [0, 1, 1], [0, 1, 0]]);assert.deepEqual(gameOfLife([[1, 1], [1, 0]]), [[1, 1], [1, 1]]);
15

Pascal's triangle#

row[i][j] = row[i-1][j-1] + row[i-1][j]; edges are 1

O(n^2)
JS
function pascal(numRows) {  const out = [];  for (let i = 0; i < numRows; i++) {    const row = new Array(i + 1).fill(1);    for (let j = 1; j < i; j++) row[j] = out[i - 1][j - 1] + out[i - 1][j];    out.push(row);  }  return out;}
Verified by 2 asserts
JS
assert.deepEqual(pascal(5), [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]);assert.deepEqual(pascal(0), []);
16

Word search (grid DFS + backtrack)#

mark '#', try 4 dirs, restore

O(mn * 3^L)
JS
function exist(board, word) {  const dfs = (r, c, i) => {    if (i === word.length) return true;    if (!inBounds(board, r, c) || board[r][c] !== word[i]) return false;    const ch = board[r][c];    board[r][c] = '#';    const ok = DIRS4.some(([dr, dc]) => dfs(r + dr, c + dc, i + 1));    board[r][c] = ch;    return ok;  };  for (let r = 0; r < board.length; r++) for (let c = 0; c < board[0].length; c++) if (dfs(r, c, 0)) return true;  return false;}const wb = [['A', 'B', 'C', 'E'], ['S', 'F', 'C', 'S'], ['A', 'D', 'E', 'E']];
Verified by 2 asserts
JS
assert.equal(exist(wb, 'SEE'), true);assert.equal(exist(wb, 'ABCB'), false);

See also