Skip to main content

Matrix and 2D Arrays

Matrix Traversal and Manipulation

Written by Updated

Grid problems are graph problems with implicit edges. Before any of that, though, you have to create the grid correctly - and the obvious way is wrong.

The bug to get out of the way first

Every row is the same array

javascript

// WRONG — fill() copies the reference, not the array.
const grid = new Array(3).fill(new Array(3).fill(0))
grid[0][0] = 1
console.log(grid)
// [[1,0,0], [1,0,0], [1,0,0]]  — all three rows changed

// RIGHT — a fresh array per row.
const grid2 = Array.from({ length: 3 }, () => new Array(3).fill(0))
grid2[0][0] = 1
console.log(grid2)
// [[1,0,0], [0,0,0], [0,0,0]]

This catches people constantly, and the symptom - writes appearing in rows you never touched - looks like a logic bug rather than a construction bug.

Transpose and rotate

Rotate 90 degrees in place

javascript

function rotate(matrix) {
  const n = matrix.length

  // Transpose: swap across the diagonal.
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      ;[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]
    }
  }

  // Then reverse each row.
  for (const row of matrix) row.reverse()

  return matrix
}

console.log(rotate([[1,2,3], [4,5,6], [7,8,9]]))
// [[7,4,1], [8,5,2], [9,6,3]]

Transpose then reverse rows gives clockwise; transpose then reverse columns gives anticlockwise. j = i + 1 matters - starting at 0 swaps everything twice and undoes the work.

Spiral traversal

Spiral order

javascript

function spiralOrder(matrix) {
  if (matrix.length === 0) return []

  const out = []
  let top = 0
  let bottom = matrix.length - 1
  let left = 0
  let right = matrix[0].length - 1

  while (top <= bottom && left <= right) {
    for (let c = left; c <= right; c++) out.push(matrix[top][c])
    top++

    for (let r = top; r <= bottom; r++) out.push(matrix[r][right])
    right--

    // Guard: the row may already have been consumed above.
    if (top <= bottom) {
      for (let c = right; c >= left; c--) out.push(matrix[bottom][c])
      bottom--
    }

    if (left <= right) {
      for (let r = bottom; r >= top; r--) out.push(matrix[r][left])
      left++
    }
  }

  return out
}

console.log(spiralOrder([[1,2,3], [4,5,6], [7,8,9]]))
// [1,2,3,6,9,8,7,4,5]

The two guards are not optional. On a single-row or single-column matrix, without them the last two loops re-read cells already collected.

Grids as graphs

Counting islands

javascript

function countIslands(grid) {
  if (grid.length === 0) return 0

  const rows = grid.length
  const cols = grid[0].length
  let count = 0

  function sink(r, c) {
    if (r < 0 || r >= rows || c < 0 || c >= cols) return
    if (grid[r][c] !== "1") return

    grid[r][c] = "0"   // mark visited by overwriting

    sink(r + 1, c)
    sink(r - 1, c)
    sink(r, c + 1)
    sink(r, c - 1)
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === "1") {
        count++
        sink(r, c)
      }
    }
  }

  return count
}

console.log(countIslands([["1", "1", "0"], ["0", "1", "0"], ["0", "0", "1"]]))  // 2

This is DFS on an implicit graph. Overwriting the cell is the visited set. If mutating the input is not allowed, keep a separate visited grid - and on a very large grid use the iterative version, since recursion here can go rows × cols deep.

Neighbours without four copies of the same code

A direction array turns the four-way or eight-way neighbour check into one loop, and makes the bounds check impossible to get inconsistently wrong.

Directions, bounds, and flood fill

javascript

const DIRECTIONS = [[-1, 0], [1, 0], [0, -1], [0, 1]]

function neighbours(grid, row, col) {
  const out = []
  for (const [dr, dc] of DIRECTIONS) {
    const r = row + dr
    const c = col + dc
    if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
      out.push([r, c])
    }
  }
  return out
}

// Count islands by sinking each one as it is found.
function countIslands(grid) {
  let count = 0

  function sink(row, col) {
    if (row < 0 || row >= grid.length) return
    if (col < 0 || col >= grid[0].length) return
    if (grid[row][col] !== "1") return

    grid[row][col] = "0"                       // mark visited in place
    for (const [r, c] of neighbours(grid, row, col)) sink(r, c)
  }

  for (let row = 0; row < grid.length; row++) {
    for (let col = 0; col < grid[0].length; col++) {
      if (grid[row][col] === "1") {
        count++
        sink(row, col)
      }
    }
  }

  return count
}

console.log(countIslands([["1", "0"], ["0", "1"]]))  // 2

Rotating in place

Rotating a square matrix 90° clockwise is a transpose followed by reversing each row. Both steps are in place, so no second grid is needed - and stating it as two named steps is far easier to get right than deriving the index arithmetic directly.

One trap when building grids: new Array(n).fill([]) puts the same array in every row, so writing to one row writes to all of them. Use Array.from({ length: n }, () => []) instead.