Loading learning content...
You've learned to identify recursive structure, decompose problems, and combine solutions. Now it's time to cement these skills through deliberate practice.
This page presents a curated collection of problems organized by pattern. For each problem, we'll analyze the recursive structure, design the solution, and implement it. The goal isn't just to solve these specific problems—it's to internalize the patterns so deeply that you recognize them instantly in new contexts.
After working through this material, you'll have a robust mental library of recursive problem patterns that you can draw from throughout your career.
For maximum learning: (1) Read the problem statement, (2) Try to identify the pattern before reading the analysis, (3) Attempt the solution yourself, (4) Compare with the provided solution, (5) Note any differences in approach.
Before diving into problems, let's organize the major recursive patterns you should recognize:
Each pattern has signature characteristics and typical problem types.
| Pattern | Key Signal | Typical Problems |
|---|---|---|
| Linear | Process elements sequentially | List operations, string processing |
| Binary | Natural halving | Search, balanced trees, merge sort |
| Tree | Hierarchical data | All tree operations |
| Backtracking | Try-and-undo | Puzzles, constraint satisfaction |
| Divide-Conquer | Independent subproblems | Sorting, optimization |
| Accumulator | Need running state | Path tracking, tail recursion |
| Mutual | Alternating rules | Parsers, state machines |
| Generate-Test | Enumerate then filter | Combinatorics |
Trees are the quintessential recursive structure. Every tree problem follows the pattern:
solve(node) = combine(node.value, solve(node.left), solve(node.right))
Problem: Given two binary trees, determine if they are structurally identical with the same values.
Pattern Recognition:
12345678910111213141516171819202122232425262728
interface TreeNode { value: number; left?: TreeNode; right?: TreeNode;} function areIdentical(t1: TreeNode | undefined, t2: TreeNode | undefined): boolean { // Base case 1: both empty if (!t1 && !t2) return true; // Base case 2: one empty, one not if (!t1 || !t2) return false; // Recursive case: check current + both subtrees return ( t1.value === t2.value && areIdentical(t1.left, t2.left) && areIdentical(t1.right, t2.right) );} // Testconst tree1: TreeNode = { value: 1, left: { value: 2 }, right: { value: 3 } };const tree2: TreeNode = { value: 1, left: { value: 2 }, right: { value: 3 } };const tree3: TreeNode = { value: 1, left: { value: 2 }, right: { value: 4 } }; console.log(areIdentical(tree1, tree2)); // trueconsole.log(areIdentical(tree1, tree3)); // falseProblem: Mirror a binary tree by swapping left and right children at every node.
Pattern Recognition:
1234567891011121314151617181920212223242526272829303132333435
interface TreeNode { value: number; left?: TreeNode; right?: TreeNode;} function invertTree(node: TreeNode | undefined): TreeNode | undefined { // Base case: empty tree if (!node) return undefined; // Recurse on children, then swap const invertedLeft = invertTree(node.left); const invertedRight = invertTree(node.right); // Construct inverted node (children swapped) return { value: node.value, left: invertedRight, // Right becomes left right: invertedLeft, // Left becomes right };} // In-place version (modifies original tree)function invertTreeInPlace(node: TreeNode | undefined): TreeNode | undefined { if (!node) return undefined; // Swap children [node.left, node.right] = [node.right, node.left]; // Recurse invertTreeInPlace(node.left); invertTreeInPlace(node.right); return node;}Backtracking explores possibilities by making choices, recursing, and undoing choices if they lead to dead ends.
Problem: Given a list of distinct integers, return all possible permutations.
Pattern Recognition:
123456789101112131415161718192021222324252627282930
function permutations(nums: number[]): number[][] { const results: number[][] = []; function backtrack(current: number[], remaining: number[]) { // Base case: all elements placed if (remaining.length === 0) { results.push([...current]); return; } // Try each remaining element in next position for (let i = 0; i < remaining.length; i++) { // Choose current.push(remaining[i]); const newRemaining = [...remaining.slice(0, i), ...remaining.slice(i + 1)]; // Explore backtrack(current, newRemaining); // Unchoose (backtrack) current.pop(); } } backtrack([], nums); return results;} console.log(permutations([1, 2, 3]));// [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]Problem: Place N queens on an N×N chessboard so no two queens attack each other.
Pattern Recognition:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
function solveNQueens(n: number): string[][] { const results: string[][] = []; const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.') ); function isValid(row: number, col: number): boolean { // Check column for (let r = 0; r < row; r++) { if (board[r][col] === 'Q') return false; } // Check upper-left diagonal for (let r = row - 1, c = col - 1; r >= 0 && c >= 0; r--, c--) { if (board[r][c] === 'Q') return false; } // Check upper-right diagonal for (let r = row - 1, c = col + 1; r >= 0 && c < n; r--, c++) { if (board[r][c] === 'Q') return false; } return true; } function backtrack(row: number) { // Base case: all queens placed if (row === n) { results.push(board.map(r => r.join(''))); return; } // Try each column in current row for (let col = 0; col < n; col++) { if (isValid(row, col)) { board[row][col] = 'Q'; // Choose backtrack(row + 1); // Explore board[row][col] = '.'; // Unchoose } } } backtrack(0); return results;} console.log(solveNQueens(4));// [[".Q..", "...Q", "Q...", "..Q."], ["..Q.", "Q...", "...Q", ".Q.."]]Divide-and-conquer splits problems into independent subproblems, solves them, and merges results.
Problem: Count pairs (i, j) where i < j but array[i] > array[j]. This measures how 'unsorted' an array is.
Pattern Recognition:
123456789101112131415161718192021222324252627282930313233343536373839
function countInversions(arr: number[]): number { // Returns [sortedArray, inversionCount] function mergeSort(nums: number[]): [number[], number] { if (nums.length <= 1) return [nums, 0]; const mid = Math.floor(nums.length / 2); const [leftSorted, leftInv] = mergeSort(nums.slice(0, mid)); const [rightSorted, rightInv] = mergeSort(nums.slice(mid)); const [merged, splitInv] = mergeAndCount(leftSorted, rightSorted); return [merged, leftInv + rightInv + splitInv]; } function mergeAndCount(left: number[], right: number[]): [number[], number] { const result: number[] = []; let inversions = 0; let i = 0, j = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result.push(left[i++]); } else { result.push(right[j++]); // All remaining elements in left are > right[j] inversions += left.length - i; } } while (i < left.length) result.push(left[i++]); while (j < right.length) result.push(right[j++]); return [result, inversions]; } return mergeSort(arr)[1];} console.log(countInversions([2, 4, 1, 3, 5])); // 3: (2,1), (4,1), (4,3)console.log(countInversions([5, 4, 3, 2, 1])); // 10: fully reversedMany recursive solutions have overlapping subproblems—the same subproblem is solved multiple times. This is where recursion meets dynamic programming.
Problem: Compute the nth Fibonacci number efficiently.
Pattern Recognition:
123456789101112131415161718192021222324252627282930313233
// Naive recursive - O(2^n) - exponential!function fibNaive(n: number): number { if (n <= 1) return n; return fibNaive(n - 1) + fibNaive(n - 2);} // With memoization - O(n) - each subproblem solved oncefunction fibMemo(n: number, memo: Map<number, number> = new Map()): number { if (n <= 1) return n; if (memo.has(n)) return memo.get(n)!; const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo); memo.set(n, result); return result;} // Bottom-up DP - O(n) time, O(1) spacefunction fibDP(n: number): number { if (n <= 1) return n; let prev2 = 0, prev1 = 1; for (let i = 2; i <= n; i++) { const current = prev1 + prev2; prev2 = prev1; prev1 = current; } return prev1;} console.log(fibMemo(40)); // 102334155 - instantconsole.log(fibDP(40)); // 102334155 - instant// fibNaive(40) would take several seconds!Memoization transforms exponential recursive solutions into polynomial ones by caching results. This is often the easiest path to a DP solution: start with pure recursion, identify overlapping subproblems, add a cache. The recursive structure guides your thinking; memoization handles efficiency.
For each problem below, identify the recursive pattern before looking at the solution approach.
Given a nested list of integers, return a flat list of all integers.
Pattern: Tree recursion (nested structure = tree) Decomposition: For each element, if it's a number, include it; if it's a list, flatten it recursively.
Given a string of digits 2-9, return all letter combinations the digits could represent.
Pattern: Backtracking/Generate-and-test Decomposition: For each digit, try each possible letter, recurse on remaining digits.
Find the contiguous subarray with the largest sum.
Pattern: Divide-and-conquer Decomposition: Max subarray is in left half, right half, or crosses the middle. Find all three, return max.
Given a string of digits, count ways to decode it as letters (1='A', 26='Z').
Pattern: Linear recursion with overlapping subproblems (DP) Decomposition: Take 1 digit + decode rest, OR take 2 digits (if valid) + decode rest.
| Problem | Primary Pattern | Key Insight |
|---|---|---|
| Flatten nested list | Tree recursion | Nested structure = implicit tree |
| Phone number letters | Backtracking | Multiple choices per position |
| Max subarray (D&C) | Divide-conquer | Cross-boundary case is key |
| Decode ways | DP/Memoization | Overlapping subproblems |
Congratulations! You've completed the Recursive Problem-Solving Strategies module. You now have a comprehensive framework for approaching any recursive problem: identify structure, choose decomposition strategy, design combination logic, and recognize patterns. Continue practicing with diverse problems to cement these skills into lasting intuition.