Matrix problems
A grid is an array of arrays — every trick here is index bookkeeping, done carefully.
Traversal direction — the pattern behind spiral order
function spiralOrder(matrix) {
const result = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (let c = left; c <= right; c++) result.push(matrix[top][c]);
top++;
for (let r = top; r <= bottom; r++) result.push(matrix[r][right]);
right--;
if (top <= bottom) { // guard: this row may already be consumed
for (let c = right; c >= left; c--) result.push(matrix[bottom][c]);
bottom--;
}
if (left <= right) { // guard: this column may already be consumed
for (let r = bottom; r >= top; r--) result.push(matrix[r][left]);
left++;
}
}
return result;
}
⚠ The two guards aren't optional
On a non-square matrix (e.g. a single row, or a single column), skipping
the
if (top <= bottom) / if (left <= right)
checks re-visits cells that the earlier two loops already covered —
this is the single most common bug in spiral-order implementations.
In-place rotation — 90° with no extra matrix
Rotating 90° clockwise decomposes into two simpler, well-known operations: transpose (flip across the diagonal), then reverse each row.
function rotate(matrix) {
const n = matrix.length;
// transpose: swap matrix[r][c] with matrix[c][r]
for (let r = 0; r < n; r++) {
for (let c = r + 1; c < n; c++) { // c starts at r+1 — never touch the diagonal or repeat a swap
[matrix[r][c], matrix[c][r]] = [matrix[c][r], matrix[r][c]];
}
}
// reverse each row
for (const row of matrix) row.reverse();
}
Grid as a graph — search patterns from the graph chapter, reused
Any grid problem involving "connected region," "flood fill," or "shortest path between cells" is the graph-traversal chapters applied directly: each cell is a node, each of its up-to-4 orthogonal neighbors is an edge.
function numIslands(grid) {
const rows = grid.length, cols = grid[0].length;
let islands = 0;
function sink(r, c) {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== "1") return;
grid[r][c] = "0"; // mark visited by mutating the grid — avoids a separate visited set
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") {
islands++;
sink(r, c); // flood-fill the whole island so it's never counted twice
}
}
}
return islands;
}
Say it like this → "I'll treat each cell as a
graph node with up to four neighbors and reuse a flood-fill DFS — this
is the exact same connected-components idea from the graph chapter, just
with grid coordinates standing in for an adjacency list."
Recognizing it in an unseen problem
- "Spiral," "rotate," "transpose," "diagonal" → boundary/index bookkeeping, work out the pattern on paper first
- "Islands," "regions," "flood fill," "shortest path in a grid" → it's a graph problem wearing a grid costume
- In-place mutation requested → look for a decomposition into two or more simpler, already-known transformations (like rotate = transpose + reverse)
- Always double-check boundary conditions on non-square grids — single row/column inputs break naive boundary logic first
Practice this layer
Opens in the editor — write it, run it, and check it against real tests.