Part 4 · Lesson 25 · 26 min read
2-D Arrays and Matrices
Rows and columns: transpose, rotate, spiral order, set zeroes and search a matrix.
On this page
- An array of rows
- Creating a matrix (and the shared-row trap)
- Looping: rows, columns, diagonals, neighbours
- Row and column sums
- Transpose and rotate by 90°
- Spiral order with four boundaries
- Set matrix zeroes
- Searching a sorted matrix
- Traced: the staircase search
- Practice questions (7)
- Make it stick
- Part 4 complete — what's next
An array of rows
A matrix (also called a grid) is a table of values arranged in rows and columns, like a spreadsheet. In JavaScript it is an array whose items are also arrays, one array for each row. m[r][c] means “go to row r, then take column c”. It is the nested-loop grid from Lesson 6, now holding data. Images, game boards, maps and spreadsheets are all matrices. Part 13 will treat grids as graphs (points joined by links).
Creating a matrix (and the shared-row trap)
const rows = 3, cols = 4;// Correct: a new row array for every rowconst grid = Array.from({ length: rows }, () => new Array(cols).fill(0));grid[0][1] = 5;console.log(grid[0][1], grid[1][1]); // 5 0// Trap: fill() puts the SAME row object in every slotconst bad = new Array(rows).fill(new Array(cols).fill(0));bad[0][1] = 5;console.log(bad[0][1], bad[1][1]); // 5 5 ← every "row" changednew Array(rows).fill(row) puts a reference to the same row in every slot (Lesson 8 explained references: a reference is an arrow that points to one object, not a copy of it). So there is only one row, shown many times. Changing one cell then changes that column in every row. Use Array.from({ length: rows }, () => new Array(cols).fill(0)) instead. It runs the function once for each row, so every row is a new array.
Looping: rows, columns, diagonals, neighbours
const m = [ [1, 2, 3], [4, 5, 6], [7, 8, 9],];const rows = m.length, cols = m[0].length;for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { // m[r][c] — row first, then column }}// Diagonals of a square matrixlet main = 0, anti = 0;for (let i = 0; i < rows; i++) { main += m[i][i]; // top-left to bottom-right anti += m[i][cols - 1 - i]; // top-right to bottom-left}console.log(main, anti); // 15 15// The four neighbours of (r, c), skipping cells outside the gridconst DIRS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; // up, down, left, rightfunction neighbours(r, c) { const out = []; for (const [dr, dc] of DIRS) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) out.push(m[nr][nc]); } return out;}console.log(neighbours(0, 0)); // [ 4, 2 ]console.log(neighbours(1, 1)); // [ 2, 8, 4, 6 ]The direction array DIRS is a small list of steps: each pair is how much to change the row and the column. It is worth learning by heart. It turns “check up, down, left and right” into one short loop. The bounds check (the if that tests the new position is inside the grid) stops you from reading outside the grid. Reading outside is the most common matrix bug. You will use this pattern in every grid problem in Part 13.
Row and column sums
const m = [ [3, 1, 2], [0, 4, 5],];const rowSums = m.map((row) => row.reduce((a, b) => a + b, 0));const colSums = new Array(m[0].length).fill(0);for (const row of m) { for (let c = 0; c < row.length; c++) colSums[c] += row[c];}console.log(rowSums, colSums); // [ 6, 9 ] [ 3, 5, 7 ]Visiting every cell once takes O(rows × cols) time. This is the number of cells, which is the size of the input. So it is linear time for a matrix.
Transpose and rotate by 90°
The transpose of a matrix turns its rows into columns: t[c][r] = m[r][c]. To rotate a square matrix 90° clockwise in place (using the same matrix, with no second one), transpose it and then reverse each row:
| Step | Row 0 | Row 1 | Row 2 |
|---|---|---|---|
| start | 1 2 3 | 4 5 6 | 7 8 9 |
| transpose | 1 4 7 | 2 5 8 | 3 6 9 |
| reverse each row | 7 4 1 | 8 5 2 | 9 6 3 |
For anticlockwise, reverse each row first and then transpose. Another way is to transpose and then reverse the order of the rows.
function rotate(m) { const n = m.length; // 1. transpose: swap across the main diagonal (only j > i, or each pair swaps twice) 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. reverse every row for (const row of m) row.reverse(); return m;}console.log(rotate([[1, 2, 3], [4, 5, 6], [7, 8, 9]]));// [ [ 7, 4, 1 ], [ 8, 5, 2 ], [ 9, 6, 3 ] ]Spiral order with four boundaries
Reading a matrix in a spiral means: right along the top, down the right side, left along the bottom, up the left side, then repeat on the inside. This tests whether you handle boundaries carefully. Keep four boundaries (top, bottom, left, right). After you read a side, move that boundary one step inwards:
function spiralOrder(m) { const 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]); // → along the top top++; for (let r = top; r <= bottom; r++) out.push(m[r][right]); // ↓ down the right right--; if (top <= bottom) { for (let c = right; c >= left; c--) out.push(m[bottom][c]); // ← along the bottom bottom--; } if (left <= right) { for (let r = bottom; r >= top; r--) out.push(m[r][left]); // ↑ up the left left++; } } return out;}console.log(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 ]The two if checks matter when the matrix is not square. After you read the top row and the right column, what is left may be a single row or a single column. Without the checks, you would read it twice.
Set matrix zeroes
The task: if a cell is 0, set its whole row and column to 0. If you write zeros while you are still scanning, the new zeros look like original zeros and spread too far. So do it in two passes. First scan and remember which rows and columns to clear. Then write the zeros:
function setZeroes(m) { const zeroRows = new Set(), zeroCols = new Set(); for (let r = 0; r < m.length; r++) // pass 1: find the zeros for (let c = 0; c < m[0].length; c++) if (m[r][c] === 0) { zeroRows.add(r); zeroCols.add(c); } for (let r = 0; r < m.length; r++) // pass 2: write the zeros for (let c = 0; c < m[0].length; c++) if (zeroRows.has(r) || zeroCols.has(c)) m[r][c] = 0; return m;}console.log(setZeroes([[1, 1, 1], [1, 0, 1], [1, 1, 1]]));// [ [ 1, 0, 1 ], [ 0, 0, 0 ], [ 1, 0, 1 ] ]This takes O(rows × cols) time and O(rows + cols) extra space. A common follow-up asks for O(1) extra space (a fixed amount, however big the matrix is). The idea is to use the first row and first column of the matrix itself as the markers (Practice question 5).
Searching a sorted matrix
There are two common kinds of “sorted matrix”. They need different searches:
- Fully sorted (each row is sorted, and each row starts after the previous one ends): this is one sorted list folded into rows. Use binary search (a search that checks the middle value and throws away half of the range each time) over the indices 0 … rows × cols − 1. Turn an index into a cell with
row = Math.floor(i / cols)andcol = i % cols. It takes O(log(rows × cols)) time. - Rows and columns sorted separately (every row is sorted, and every column is sorted, but rows do not follow each other): start at the top-right corner and walk like a staircase. It takes O(rows + cols) time.
// Rows sorted, and each row starts after the previous row ends:// the matrix is really one sorted list of rows × cols values.function searchMatrix(m, target) { const rows = m.length, cols = m[0].length; let lo = 0, hi = rows * cols - 1; while (lo <= hi) { const mid = Math.floor((lo + hi) / 2); const v = m[Math.floor(mid / cols)][mid % cols]; // 1-D index → (row, column) if (v === target) return true; if (v < target) lo = mid + 1; else hi = mid - 1; } return false;}console.log(searchMatrix([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 3)); // trueconsole.log(searchMatrix([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 13)); // falseLesson 28 explains binary search in full. Here, look only at how the index is converted to a row and a column.
Traced: the staircase search
From here, moving left gives smaller values and moving down gives bigger values. So each direction has one clear meaning.
1const m = [2 [1, 4, 7, 11],3 [2, 5, 8, 12],4 [3, 6, 9, 16],5 [10, 13, 14, 17],6];7const target = 9;▶let r = 0, c = m[0].length - 1;9while (r < m.length && c >= 0) {10 if (m[r][c] === target) break;11 if (m[r][c] > target) c--;12 else r++;13}14console.log(r, c);| target | 9 |
| r | 0 |
| c | 3 |
| value | 11 |
Practice questions
Draw a small matrix on paper for every question. Most matrix bugs are index mistakes, and a drawing shows them at once.
Return the sum of both diagonals of a square matrix, counting the centre once.
Hint 1
Show the solution with explanations
Add both diagonal cells for each row, skipping the second when it is the same cell.
function diagonalSum(mat) { const n = mat.length; let sum = 0; for (let i = 0; i < n; i++) { sum += mat[i][i]; if (i !== n - 1 - i) sum += mat[i][n - 1 - i]; } return sum;}console.log(diagonalSum([[1, 2, 3], [4, 5, 6], [7, 8, 9]])); // 25console.log(diagonalSum([[5]])); // 5O(n) time. Only the diagonal cells are visited, not all n² cells.
Do not use a double loop with an if to find the diagonals. Use the indices directly. (LeetCode 1572.)
Return the transpose of a matrix (rows become columns).
Hint 1
cols rows and rows columns. t[c][r] = m[r][c].Show all 2 solutions with explanations
Create a cols × rows matrix and copy each cell to its swapped position.
function transpose(m) { const rows = m.length, cols = m[0].length; const t = Array.from({ length: cols }, () => new Array(rows)); for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) t[c][r] = m[r][c]; return t;}console.log(transpose([[1, 2, 3], [4, 5, 6]])); // [ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]O(rows × cols). A non-square matrix cannot easily be transposed in place, because its shape changes (rows × cols becomes cols × rows).
Column c of m is row c of the result.
const transpose = (m) => m[0].map((_, c) => m.map((row) => row[c]));console.log(transpose([[1, 2, 3], [4, 5, 6]])); // [ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]Same cost, but shorter. Make sure you can also write the loop version.
(LeetCode 867.)
Rotate an n × n matrix 90° clockwise, in place.
Hint 1
Hint 2
Show all 2 solutions with explanations
Write each value to its new position in a new matrix.
function rotate(m) { const n = m.length; const out = Array.from({ length: n }, () => new Array(n)); for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) out[c][n - 1 - r] = m[r][c]; return out;}console.log(rotate([[1, 2, 3], [4, 5, 6], [7, 8, 9]])); // [ [ 7, 4, 1 ], [ 8, 5, 2 ], [ 9, 6, 3 ] ]O(n²) time and O(n²) extra space. This is not allowed when the question says “in place”.
The method from the lesson.
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(); return m;}console.log(rotate([[5, 1, 9, 11], [2, 4, 8, 10], [13, 3, 6, 7], [15, 14, 12, 16]]));// [ [ 15, 13, 2, 5 ], [ 14, 3, 4, 1 ], [ 12, 6, 8, 9 ], [ 16, 7, 10, 11 ] ]O(n²) time, O(1) extra space. The inner loop starts at j = i + 1 so that each pair is swapped only once.
The in-place version is expected. (LeetCode 48.)
Return all values of the matrix in spiral order.
Hint 1
Hint 2
Show the solution with explanations
The method from the lesson.
function spiralOrder(m) { const 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;}console.log(spiralOrder([[1, 2, 3], [4, 5, 6], [7, 8, 9]])); // [ 1, 2, 3, 6, 9, 8, 7, 4, 5 ]console.log(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 ]| Side | Reads | Boundary after |
|---|---|---|
| top → | 1 2 3 4 | top = 1 |
| right ↓ | 8 12 | right = 2 |
| bottom ← | 11 10 9 | bottom = 1 |
| left ↑ | 5 | left = 1 |
| top → | 6 7 | top = 2 (loop ends) |
O(rows × cols) time. Test with a single row, a single column and a non-square matrix. (LeetCode 54.)
If a cell is 0, set its entire row and column to 0, in place.
Hint 1
Hint 2
Show all 2 solutions with explanations
The version from the lesson.
function setZeroes(m) { const rows = new Set(), cols = new Set(); m.forEach((row, r) => row.forEach((v, c) => { if (v === 0) { rows.add(r); cols.add(c); } })); m.forEach((row, r) => row.forEach((_, c) => { if (rows.has(r) || cols.has(c)) row[c] = 0; })); return m;}console.log(setZeroes([[1, 1, 1], [1, 0, 1], [1, 1, 1]])); // [ [ 1, 0, 1 ], [ 0, 0, 0 ], [ 1, 0, 1 ] ]O(rows × cols) time, O(rows + cols) space.
- Remember whether row 0 and column 0 contain a zero.
- For every other zero at (r, c), set
m[r][0] = 0andm[0][c] = 0. - Clear inner cells using those marks, then clear row 0 and column 0 if needed.
function setZeroes(m) { const R = m.length, C = m[0].length; const firstRow = m[0].some((v) => v === 0); const firstCol = m.some((row) => row[0] === 0); for (let r = 1; r < R; r++) for (let c = 1; c < C; c++) if (m[r][c] === 0) { m[r][0] = 0; m[0][c] = 0; } for (let r = 1; r < R; r++) for (let c = 1; c < C; c++) if (m[r][0] === 0 || m[0][c] === 0) m[r][c] = 0; if (firstRow) m[0].fill(0); if (firstCol) for (let r = 0; r < R; r++) m[r][0] = 0; return m;}console.log(setZeroes([[1, 1, 1], [1, 0, 1], [1, 1, 1]]));// [ [ 1, 0, 1 ], [ 0, 0, 0 ], [ 1, 0, 1 ] ]console.log(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 ] ]O(1) extra space. The two flags are needed because the marks from other cells overwrite row 0 and column 0.
Give the Sets version first. The marker version is the follow-up. (LeetCode 73.)
Each row is sorted, and the first value of each row is greater than the last value of the previous row. Return whether target is in the matrix.
Hint 1
Hint 2
m[Math.floor(i / cols)][i % cols].Show the solution with explanations
Search indices 0 … rows × cols − 1 and convert each index to a cell.
function searchMatrix(m, target) { const cols = m[0].length; let lo = 0, hi = m.length * cols - 1; while (lo <= hi) { const mid = Math.floor((lo + hi) / 2); const v = m[Math.floor(mid / cols)][mid % cols]; if (v === target) return true; if (v < target) lo = mid + 1; else hi = mid - 1; } return false;}const M = [[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]];console.log(searchMatrix(M, 3)); // trueconsole.log(searchMatrix(M, 13)); // falseO(log(rows × cols)) time. The index conversion is useful in many other problems too.
(LeetCode 74.)
Each row is sorted left to right and each column is sorted top to bottom. Return whether target is in the matrix.
Hint 1
Hint 2
Show the solution with explanations
Too big → move left. Too small → move down.
function searchMatrix(m, target) { let r = 0, 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;}const M = [[1, 4, 7, 11], [2, 5, 8, 12], [3, 6, 9, 16], [10, 13, 14, 17]];console.log(searchMatrix(M, 9)); // trueconsole.log(searchMatrix(M, 20)); // falseO(rows + cols) time, O(1) extra space. The bottom-left corner works too, with the directions swapped. The top-left and bottom-right corners do not work, because from there both moves go in the same direction (both make the value bigger, or both make it smaller), so you cannot tell which way to go.
Know which kind of “sorted matrix” you have before choosing the search. (LeetCode 240.)
Make it stick
- Write the safe way to create a rows × cols grid, and explain the
filltrap. - Write the four-direction neighbour loop with its bounds check.
- Rotate a 3 × 3 matrix by hand: transpose, then reverse rows.
- Explain why the staircase search starts at the top-right corner.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
Part 4 complete — what's next
You now know the main array patterns: read/write pointers, prefix sums, two pointers, fixed and variable windows, Kadane's algorithm and matrix traversal. Together they solve many of the array questions in interviews.
Part 5 — Hashing Patterns goes back to Maps and Sets and uses them to solve problems: Two Sum, “have I seen this before?”, longest consecutive sequence, grouping anagrams and finding the majority element.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series