Matrix Algorithms: Practice Problems with Solutions
Welcome to the practice problems for matrix algorithms. If you need a refresher on the code, the code templates have the patterns in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.
Recommended Study Order
The problems are ordered by difficulty, but the progression matters as much as the individual solutions.
- Reshape the Matrix teaches the core coordinate mapping. Flattening a 2D position into a 1D index and back is the skill every other problem builds on.
- Transpose Matrix introduces the index swap that rotation later uses. Get comfortable with
result[j][i] = matrix[i][j]. - Island Perimeter is pure neighbor counting. The bounds checks from the concept page do all the work here.
- Rotate Image combines transpose with a row reversal into one in-place transform. The two steps are easy. Seeing that they compose is the lesson.
- Set Matrix Zeroes replaces extra memory with marker cells inside the matrix. This is the first real optimization problem in the set.
- Spiral Matrix exercises boundary tracking with four shrinking pointers. The order of the guards matters here.
- Longest Increasing Path in a Matrix adds memoization on top of grid DFS. It is the hardest problem in the set and the one that most resembles a real interview question.
Grid-as-graph problems like Number of Islands live on the Union Find problems page , because that pattern handles connected components directly. Read that page next if island counting is your target.
Easy Problems
1. Reshape the Matrix
LeetCode 566 | Difficulty: Easy
Brief: Transform an m x n matrix into an r x c matrix keeping row-major order. If the cell count differs, return the original matrix.
Why this pattern: The only moving part is the coordinate mapping. A 1D position k maps to row k / c and column k % c, and every cell keeps its position in the flattened order.
Hint: Walk the input in row-major order with a single counter, and write each value to the position the counter implies in the output shape.
Complexity:
Visual:
graph LR
A["2x2: 1 2 / 3 4"] --> B["Flatten: 1 2 3 4"]
B --> C["Reshape 1x4: [1, 2, 3, 4]"]
Code:
var matrixReshape = function(mat, r, c) {
const m = mat.length, n = mat[0].length;
if (m * n !== r * c) return mat;
const flat = mat.flat();
const result = [];
for (let i = 0; i < r; i++) {
result.push(flat.slice(i * c, (i + 1) * c));
}
return result;
};The single counter k replaces a pair of nested index variables. Every cell of the input is visited once and written to the position k / c, k % c in the output. The cell count check at the top is the whole trick. If the input cannot fill the requested shape exactly, the answer is the original matrix.
2. Transpose Matrix
LeetCode 867 | Difficulty: Easy
Brief: Return the matrix flipped over its main diagonal, so rows become columns.
Why this pattern: The transpose is the purest index swap in the pattern family. Every output cell result[j][i] receives matrix[i][j].
Hint: The output has N rows and M columns, the reversed dimensions of the input. Allocate the output with the swapped dimensions first so the writes stay in bounds.
Complexity:
Visual:
graph LR
A["matrix[i][j]"] --> B["result[j][i]"]
Code:
var transpose = function(matrix) {
const m = matrix.length, n = matrix[0].length;
const res = Array.from({length: n}, () => new Array(m));
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
res[j][i] = matrix[i][j];
}
}
return res;
};The dimensions swap because a 1 x 3 row becomes a 3 x 1 column. Allocating the output with N rows and M columns before the loop keeps the inner assignment a one-liner. The index swap itself never changes, whether the matrix is square or not.
3. Island Perimeter
LeetCode 463 | Difficulty: Easy
Brief: Given a grid of 1s (land) and 0s (water), return the perimeter of the single island.
Why this pattern: Each land cell contributes four edges, and every shared edge with a neighboring land cell removes two. Only two neighbors need checking because each shared edge is counted once when you look only up and left.
Hint: Count 4 for every land cell, then subtract 2 for each land neighbor directly above and directly to the left.
Complexity:
Visual:
graph TD
A["Land cell"] --> B["+4 edges"]
B --> C{"Land above?"}
C -->|Yes| D["-2 shared edge"]
C -->|No| E["No change"]
D --> F{"Land to the left?"}
E --> F
F -->|Yes| G["-2 shared edge"]
F -->|No| H["Cell done"]
Code:
var islandPerimeter = function(grid) {
let perimeter = 0;
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
if (grid[r][c] === 1) {
perimeter += 4;
// A land neighbor above shares an edge. Check only
// up and left so each shared edge is counted once.
if (r > 0 && grid[r - 1][c] === 1) perimeter -= 2;
if (c > 0 && grid[r][c - 1] === 1) perimeter -= 2;
}
}
}
return perimeter;
};The perimeter is 4 per land cell minus 2 per shared edge. Looking only at the up and left neighbors means every shared edge is seen exactly once, from the perspective of whichever cell comes later in the scan. A single land cell surrounded by water therefore scores 4, and each connection removes exactly the two edges it covers.
Medium Problems
4. Rotate Image
LeetCode 48 | Difficulty: Medium
Brief: Rotate an n x n matrix 90 degrees clockwise in place.
Why this pattern: Rotation decomposes into two in-place steps that each follow from the coordinate rules. Transpose swaps matrix[i][j] with matrix[j][i], then reversing every row moves each column into its rotated position.
Hint: Do the transpose with the inner loop starting at j = i + 1, then reverse each row with a two-pointer swap.
Complexity:
Visual:
graph LR
A["Original"] --> B["Transpose"]
B --> C["Reverse each row"]
C --> D["Rotated 90 degrees"]
Code:
var rotate = function(matrix) {
const n = matrix.length;
// Transpose: swap each pair across the diagonal once.
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]];
}
}
// Reverse every row to complete the clockwise rotation.
for (let i = 0; i < n; i++) {
matrix[i].reverse();
}
};The transpose loop starts at j = i + 1 because swapping j < i would undo the earlier swap of the same pair. The row reversal uses the two-pointer swap from the array manipulation pattern
, applied once per row. Both steps together are the entire rotation, with no second matrix allocated.
5. Set Matrix Zeroes
LeetCode 73 | Difficulty: Medium
Brief: If a cell is zero, set its entire row and column to zero. Modify the matrix in place.
Why this pattern: The marker technique stores the rows and columns to zero in the first row and first column of the matrix itself. That is the in-place version of a separate set.
Hint: The first column needs its own flag because matrix[0][0] is shared between the first row marker and the first column marker.
Complexity:
Visual:
graph TD
Z["Cell is zero"] --> R["Mark row: matrix[i][0] = 0"]
Z --> C["Mark column: matrix[0][j] = 0"]
R --> P["Second pass zeroes marked rows and columns"]
C --> P
Code:
var setZeroes = function(matrix) {
const m = matrix.length, n = matrix[0].length;
let col0 = 1;
// Pass 1: record zeroing decisions in the first row
// and first column. col0 keeps the first column's
// status separate from matrix[0][0].
for (let i = 0; i < m; i++) {
if (matrix[i][0] === 0) col0 = 0;
for (let j = 1; j < n; j++) {
if (matrix[i][j] === 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// Pass 2: apply the markers bottom-up so the recorded
// decisions survive while the cells below are zeroed.
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 1; j--) {
if (matrix[i][0] === 0 || matrix[0][j] === 0) {
matrix[i][j] = 0;
}
}
if (col0 === 0) matrix[i][0] = 0;
}
};The first pass records which rows and columns need zeroing, using the first row and first column as storage. The second pass runs bottom-up so the recorded markers in row 0 survive while the cells below are zeroed. The col0 flag keeps the first column separate from matrix[0][0], since that single cell serves both the row marker and the column marker.
6. Spiral Matrix
LeetCode 54 | Difficulty: Medium
Brief: Return all elements of an m x n matrix in spiral order.
Why this pattern: Spiral order is boundary management. Four pointers define the remaining ring, and each pass consumes one edge of it.
Hint: After the top and right passes, the ring can collapse to a single row or a single column. Guard the left and bottom passes against that.
Complexity:
Visual:
graph TD
A["top, bottom, left, right"] --> B["Right across top, top++"]
B --> C["Down the right side, right--"]
C --> D{"top <= bottom?"}
D -->|Yes| E["Left across bottom, bottom--"]
D -->|No| H["Done"]
E --> F{"left <= right?"}
F -->|Yes| G["Up the left side, left++"]
F -->|No| H
G --> A
Code:
var spiralOrder = function(matrix) {
const result = [];
if (matrix.length === 0) return result;
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
// Right along the top row, then the top edge is done.
for (let j = left; j <= right; j++) result.push(matrix[top][j]);
top++;
// Down the right column, then the right edge is done.
for (let i = top; i <= bottom; i++) result.push(matrix[i][right]);
right--;
// The remaining grid may be a single row, so guard.
if (top <= bottom) {
for (let j = right; j >= left; j--) result.push(matrix[bottom][j]);
bottom--;
}
// Or a single column, so guard this pass too.
if (left <= right) {
for (let i = bottom; i >= top; i--) result.push(matrix[i][left]);
left++;
}
}
return result;
};Each of the four passes advances its boundary pointer after consuming the edge. The guards after the first two passes handle the collapsed cases. A single row cannot walk down, and a single column cannot walk left, so those passes are skipped exactly when the ring has shrunk to one dimension.
Hard Problems
7. Longest Increasing Path in a Matrix
LeetCode 329 | Difficulty: Hard
Brief: Find the length of the longest strictly increasing path in an m x n matrix, moving up, down, left, or right between cells.
Why this pattern: This combines grid DFS with memoization. Because paths must be strictly increasing, they can never revisit a cell, so the visited set from the flood-fill template is unnecessary. The memo table replaces it, storing the best path starting at each cell.
Hint: Write a recursive function that returns the longest path starting at a cell. Cache the result in a table so each cell computes it only once.
Complexity:
Visual:
graph TD
A["dfs(r, c)"] --> B{"Memoized?"}
B -->|Yes| C["Return cached value"]
B -->|No| D["Try neighbors with larger values"]
D --> E["best = 1 + best neighbor path"]
E --> F["Store in memo, return"]
Code:
var longestIncreasingPath = function(matrix) {
if (matrix.length === 0) return 0;
const rows = matrix.length, cols = matrix[0].length;
const memo = Array.from({length: rows}, () => new Array(cols).fill(0));
const dfs = (r, c) => {
if (memo[r][c] !== 0) return memo[r][c];
let best = 1;
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
// Only larger neighbors can extend the path,
// which also guarantees no cycles.
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& matrix[nr][nc] > matrix[r][c]) {
best = Math.max(best, 1 + dfs(nr, nc));
}
}
memo[r][c] = best;
return best;
};
let ans = 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
ans = Math.max(ans, dfs(i, j));
}
}
return ans;
};Each dfs call returns the longest path that starts at its cell. The recursion moves only to strictly larger neighbors, so there is no cycle and no visited set needed. Every cell computes its value once and the memo serves the rest of the calls, which is what keeps the total work at one visit per cell. A flat grid of equal values returns 1 for every cell, and the answer is 1, which is the right result for a grid with no increasing moves.
These seven problems cover the matrix pattern from coordinate mapping to boundary management to grid search. Start with the coordinate basics in Reshape the Matrix and Transpose Matrix, work through the in-place transforms, and finish with the memoized DFS in Longest Increasing Path. By the end, a 2D array should feel like a map you can traverse in any order the problem demands.