Module 8.1

Recursion & Backtracking

Recursion: a function that calls itself! It's like standing between two mirrors โ€” infinite reflections! Master the art of breaking down complex problems into simpler versions of themselves, then unleash the power of backtracking to explore every possibility.

55 min read
Intermediate
Interview Essential
What You'll Learn
  • Recursion Fundamentals
  • Backtracking Template
  • Subsets & Permutations
  • N-Queens Problem
  • Sudoku Solver
Contents
01

Recursion Fundamentals

Recursion

A function that calls itself to solve smaller subproblems. Every recursive solution needs a base case (termination) and recursive case (reduction toward base case).

// Recursion Template
returnType recursiveFunction(parameters) {
    // 1. Base case - when to stop
    if (baseCondition) {
        return baseResult;
    }
    
    // 2. Recursive case - solve smaller subproblem
    // 3. Combine results if needed
    return recursiveFunction(smallerProblem);
}

// Classic Examples
// Factorial: n! = n * (n-1)!
int factorial(int n) {
    if (n <= 1) return 1;           // Base case
    return n * factorial(n - 1);    // Recursive case
}

// Fibonacci: fib(n) = fib(n-1) + fib(n-2)
int fibonacci(int n) {
    if (n <= 1) return n;           // Base cases: fib(0)=0, fib(1)=1
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Sum of array
int arraySum(int[] arr, int index) {
    if (index >= arr.length) return 0;
    return arr[index] + arraySum(arr, index + 1);
}

// Reverse a string
String reverse(String s) {
    if (s.length() <= 1) return s;
    return reverse(s.substring(1)) + s.charAt(0);
}

// Binary Search - recursive
int binarySearch(int[] arr, int target, int left, int right) {
    if (left > right) return -1;
    
    int mid = left + (right - left) / 2;
    
    if (arr[mid] == target) return mid;
    if (arr[mid] < target) {
        return binarySearch(arr, target, mid + 1, right);
    }
    return binarySearch(arr, target, left, mid - 1);
}
Step-by-Step: How Recursion Works
  1. Base Case Check: Every recursive call first checks if we've reached the stopping condition
  2. Recursive Call: If not at base case, the function calls itself with a smaller problem
  3. Stack Building: Each call adds a new frame to the call stack (see diagram below)
  4. Unwinding: Once base case is hit, results bubble back up through the stack
Call Stack Visualization: factorial(4)
๐Ÿ“ž factorial(4) called
   โ”‚
   โ”œโ”€โ”€โ–ถ ๐Ÿ“ž factorial(3) called
   โ”‚       โ”‚
   โ”‚       โ”œโ”€โ”€โ–ถ ๐Ÿ“ž factorial(2) called
   โ”‚       โ”‚       โ”‚
   โ”‚       โ”‚       โ”œโ”€โ”€โ–ถ ๐Ÿ“ž factorial(1) called
   โ”‚       โ”‚       โ”‚       โ”‚
   โ”‚       โ”‚       โ”‚       โ””โ”€โ”€โ—€ returns 1 โœ… (base case!)
   โ”‚       โ”‚       โ”‚
   โ”‚       โ”‚       โ””โ”€โ”€โ—€ returns 2 ร— 1 = 2
   โ”‚       โ”‚
   โ”‚       โ””โ”€โ”€โ—€ returns 3 ร— 2 = 6
   โ”‚
   โ””โ”€โ”€โ—€ returns 4 ร— 6 = 24 

โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
โ•‘  CALL STACK (grows downward, unwinds upward)              โ•‘
โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
โ•‘  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                  โ•‘
โ•‘  โ”‚ factorial(4)        โ”‚ โ† waiting for factorial(3)      โ•‘
โ•‘  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                                  โ•‘
โ•‘  โ”‚ factorial(3)        โ”‚ โ† waiting for factorial(2)      โ•‘
โ•‘  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                                  โ•‘
โ•‘  โ”‚ factorial(2)        โ”‚ โ† waiting for factorial(1)      โ•‘
โ•‘  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                                  โ•‘
โ•‘  โ”‚ factorial(1)        โ”‚ โ† BASE CASE! Returns 1          โ•‘
โ•‘  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                  โ•‘
โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
02

Backtracking Template

Backtracking: Build solutions incrementally, abandoning ("backtracking") when current path cannot lead to valid solution. Choose โ†’ Explore โ†’ Unchoose.
// General Backtracking Template
void backtrack(State state, List<Result> results) {
    // 1. Check if we've found a valid solution
    if (isGoal(state)) {
        results.add(copy(state.solution));
        return;
    }
    
    // 2. Try each possible choice
    for (Choice choice : getChoices(state)) {
        // 3. Skip invalid choices (pruning)
        if (!isValid(choice, state)) continue;
        
        // 4. Make the choice
        state.make(choice);
        
        // 5. Explore with this choice
        backtrack(state, results);
        
        // 6. Undo the choice (backtrack)
        state.undo(choice);
    }
}

// Key Pattern: Choose โ†’ Explore โ†’ Unchoose
// - Choose: Add current element to path
// - Explore: Recurse to next decision point
// - Unchoose: Remove element to try other options
Understanding the Backtracking Pattern
  1. Check Goal: First, see if current state is a valid complete solution
  2. Generate Choices: List all possible next moves from current state
  3. Prune Invalid: Skip choices that can't possibly lead to solutions (optimization!)
  4. Choose: Make a choice and modify the state
  5. Explore: Recursively explore with the new state
  6. Unchoose: Undo the choice to restore state for trying other options
Backtracking Decision Tree: Subsets of [1,2]
                         []  โ† Start with empty set
                        / \
              Include 1?  Skip 1?
                      /       \
                   [1]         []
                  /   \       /   \
        Include 2?  Skip 2?  Include 2?  Skip 2?
              /        \        /          \
           [1,2]      [1]      [2]         []
             โœ“         โœ“        โœ“           โœ“

  All Subsets: [], [1], [2], [1,2]
  
   At each node, we CHOOSE to include or skip,
     EXPLORE both paths, then UNCHOOSE (backtrack) to parent!
03

Subsets & Combinations

// Subsets - All possible subsets of array
// Time: O(2^n), Space: O(n)
List> subsets(int[] nums) {
    List> result = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), result);
    return result;
}

void backtrack(int[] nums, int start, List path, 
               List> result) {
    // Every path is a valid subset
    result.add(new ArrayList<>(path));
    
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);              // Choose
        backtrack(nums, i + 1, path, result);  // Explore
        path.remove(path.size() - 1);   // Unchoose
    }
}

// Subsets II (with duplicates) - Skip duplicates
List> subsetsWithDup(int[] nums) {
    List> result = new ArrayList<>();
    Arrays.sort(nums);  // Sort to group duplicates
    backtrackDup(nums, 0, new ArrayList<>(), result);
    return result;
}

void backtrackDup(int[] nums, int start, List path, 
                  List> result) {
    result.add(new ArrayList<>(path));
    
    for (int i = start; i < nums.length; i++) {
        // Skip duplicates at same level
        if (i > start && nums[i] == nums[i - 1]) continue;
        
        path.add(nums[i]);
        backtrackDup(nums, i + 1, path, result);
        path.remove(path.size() - 1);
    }
}

// Combinations - Choose k elements from n
List> combine(int n, int k) {
    List> result = new ArrayList<>();
    backtrackCombine(n, k, 1, new ArrayList<>(), result);
    return result;
}

void backtrackCombine(int n, int k, int start, List path, 
                      List> result) {
    if (path.size() == k) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    // Pruning: need k-path.size() more elements
    for (int i = start; i <= n - (k - path.size()) + 1; i++) {
        path.add(i);
        backtrackCombine(n, k, i + 1, path, result);
        path.remove(path.size() - 1);
    }
}

// Combination Sum - Elements can be reused
List> combinationSum(int[] candidates, int target) {
    List> result = new ArrayList<>();
    backtrackSum(candidates, target, 0, new ArrayList<>(), result);
    return result;
}

void backtrackSum(int[] candidates, int remain, int start, 
                  List path, List> result) {
    if (remain == 0) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = start; i < candidates.length; i++) {
        if (candidates[i] > remain) continue;
        
        path.add(candidates[i]);
        // i (not i+1) allows reuse of same element
        backtrackSum(candidates, remain - candidates[i], i, path, result);
        path.remove(path.size() - 1);
    }
}
04

Permutations

// Permutations - All orderings
// Time: O(n!), Space: O(n)
List> permute(int[] nums) {
    List> result = new ArrayList<>();
    backtrackPermute(nums, new ArrayList<>(), new boolean[nums.length], result);
    return result;
}

void backtrackPermute(int[] nums, List path, boolean[] used, 
                      List> result) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        
        used[i] = true;
        path.add(nums[i]);
        backtrackPermute(nums, path, used, result);
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

// Permutations II (with duplicates)
List> permuteUnique(int[] nums) {
    List> result = new ArrayList<>();
    Arrays.sort(nums);  // Sort to handle duplicates
    backtrackUnique(nums, new ArrayList<>(), new boolean[nums.length], result);
    return result;
}

void backtrackUnique(int[] nums, List path, boolean[] used, 
                     List> result) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        
        // Skip duplicates: if previous same element wasn't used at this level
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
        
        used[i] = true;
        path.add(nums[i]);
        backtrackUnique(nums, path, used, result);
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

// Letter Combinations of Phone Number
List letterCombinations(String digits) {
    if (digits.isEmpty()) return new ArrayList<>();
    
    String[] mapping = {"", "", "abc", "def", "ghi", "jkl", 
                        "mno", "pqrs", "tuv", "wxyz"};
    List result = new ArrayList<>();
    backtrackLetters(digits, 0, new StringBuilder(), mapping, result);
    return result;
}

void backtrackLetters(String digits, int index, StringBuilder path, 
                      String[] mapping, List result) {
    if (index == digits.length()) {
        result.add(path.toString());
        return;
    }
    
    String letters = mapping[digits.charAt(index) - '0'];
    for (char c : letters.toCharArray()) {
        path.append(c);
        backtrackLetters(digits, index + 1, path, mapping, result);
        path.deleteCharAt(path.length() - 1);
    }
}
05

N-Queens & Sudoku Solver

N-Queens Problem

// N-Queens - Place N queens on NxN board
List> solveNQueens(int n) {
    List> result = new ArrayList<>();
    char[][] board = new char[n][n];
    
    for (char[] row : board) Arrays.fill(row, '.');
    
    backtrackQueens(board, 0, result);
    return result;
}

void backtrackQueens(char[][] board, int row, List> result) {
    if (row == board.length) {
        result.add(constructBoard(board));
        return;
    }
    
    for (int col = 0; col < board.length; col++) {
        if (isValidQueen(board, row, col)) {
            board[row][col] = 'Q';
            backtrackQueens(board, row + 1, result);
            board[row][col] = '.';
        }
    }
}

boolean isValidQueen(char[][] board, int row, int col) {
    // Check column above
    for (int i = 0; i < row; i++) {
        if (board[i][col] == 'Q') return false;
    }
    
    // Check upper-left diagonal
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q') return false;
    }
    
    // Check upper-right diagonal
    for (int i = row - 1, j = col + 1; i >= 0 && j < board.length; i--, j++) {
        if (board[i][j] == 'Q') return false;
    }
    
    return true;
}

List constructBoard(char[][] board) {
    List result = new ArrayList<>();
    for (char[] row : board) {
        result.add(new String(row));
    }
    return result;
}

Sudoku Solver

// Sudoku Solver
void solveSudoku(char[][] board) {
    solve(board);
}

boolean solve(char[][] board) {
    for (int row = 0; row < 9; row++) {
        for (int col = 0; col < 9; col++) {
            if (board[row][col] == '.') {
                for (char c = '1'; c <= '9'; c++) {
                    if (isValidSudoku(board, row, col, c)) {
                        board[row][col] = c;
                        
                        if (solve(board)) return true;
                        
                        board[row][col] = '.';  // Backtrack
                    }
                }
                return false;  // No valid digit found
            }
        }
    }
    return true;  // All cells filled
}

boolean isValidSudoku(char[][] board, int row, int col, char c) {
    for (int i = 0; i < 9; i++) {
        // Check row
        if (board[row][i] == c) return false;
        // Check column
        if (board[i][col] == c) return false;
        // Check 3x3 box
        int boxRow = 3 * (row / 3) + i / 3;
        int boxCol = 3 * (col / 3) + i % 3;
        if (board[boxRow][boxCol] == c) return false;
    }
    return true;
}
06

Practice Problems

Solidify your recursion and backtracking skills with these classic problems!

Easy
Fibonacci Sequence

Compute the nth Fibonacci number using recursion. Bonus: Add memoization!

fib(n) = fib(n-1) + fib(n-2)
Easy
Power of Two

Check if a number is a power of 2 using recursion. No loops allowed!

isPowerOfTwo(n) โ†’ true if n = 2^k
Medium
Generate All Subsets

Given an array, return all possible subsets. Handle duplicates too!

[1,2] โ†’ [[], [1], [2], [1,2]]
Hard
Permutations

Generate all unique permutations of an array. Track used elements!

[1,2] โ†’ [[1,2], [2,1]]

Key Takeaways

Recursion = Base + Recursive Case

Always define when to stop and how to reduce problem.

Backtracking = Choose โ†’ Explore โ†’ Unchoose

Make choice, explore, undo to try other options.

Pruning = Early Termination

Skip invalid choices early to reduce time complexity.

Subsets O(2โฟ), Perms O(n!)

Know exponential complexities for combination problems.

Knowledge Check

Question 1 of 6

What are the two essential parts of a recursive function?

Question 2 of 6

How many subsets does an array of n elements have?

Question 3 of 6

What is the backtracking pattern?

Question 4 of 6

In N-Queens, what do we check before placing a queen?

Question 5 of 6

What causes a StackOverflowError in recursion?

Question 6 of 6

What is the time complexity of generating all permutations of n elements?