DEV Community

gaurbprajapati
gaurbprajapati

Posted on

Recursion vs Backtracking – Complete Guide

πŸ”Ή 1. Recursion Basics

Recursion = A function calling itself with a smaller subproblem until a base case is reached.

General template:

void recurse(params) {
    if (base case) return;   // stopping condition
    // work
    recurse(smaller problem);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 2. Pass by Value vs Pass by Reference

  • Java:

    • Primitives (int, char, double) β†’ pass by value (a copy is passed).
    • Objects (ArrayList, HashMap) β†’ reference is passed (so changes affect original).
  • Python:

    • Everything is an object.
    • Mutable objects (list, dict, set) β†’ changes persist across function calls.
    • Immutable objects (int, str, tuple) β†’ new objects created instead of modifying.

πŸ”Ή 3. Mutable vs Immutable

  • Mutable: Can be changed after creation.
    Examples: Python list, dict; Java ArrayList, HashMap.

  • Immutable: Cannot be changed after creation.
    Examples: Python int, str, tuple; Java String, Integer.

⚑ Why does this matter?
πŸ‘‰ Because backtracking is needed only when recursion modifies a mutable object.


πŸ”Ή 4. When Do We Need Backtracking?

πŸ‘‰ Ask yourself:
Am I mutating a shared data structure?

  • βœ… Yes (mutable, like list/array) β†’ Backtracking needed

    • Pattern: add β†’ recurse β†’ remove
    • Shared object β†’ undo is required.
  • ❌ No (immutable, like string/number/new copy) β†’ No backtracking

    • Each recursive call gets its own copy.
    • No undo required.

πŸ”Ή 5. Immutable State Approach (No Backtracking)

Each recursive call gets a new snapshot of the state.

  • Safe: no undo needed.
  • Trade-off: more memory usage.

Example (Phone keypad combinations, immutable strings)

def pad(p, up):
    if not up:
        print(p)
        return
    digit = int(up[0])
    mapping = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
    for ch in mapping[digit]:
        pad(p + ch, up[1:])   # creates new string each call
Enter fullscreen mode Exit fullscreen mode

Here p + ch creates a new string each time β†’ no need to undo.


πŸ”Ή 6. Mutable State Approach (Needs Backtracking)

Recursive calls share the same object.
We must undo changes after recursion to avoid corrupting results.

  • Efficient: less memory usage.
  • Risk: must backtrack properly.

Example (Phone keypad combinations, mutable list)

def letter_combinations(digits):
    mapping = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
    ans = []
    def solve(index, path):
        if index == len(digits):
            ans.append("".join(path))
            return
        for ch in mapping[int(digits[index])]:
            path.append(ch)           # add
            solve(index+1, path)
            path.pop()                # undo (backtrack)
    solve(0, [])
    return ans
Enter fullscreen mode Exit fullscreen mode

Here path is shared β†’ after exploring one branch, we pop() to reset it.


πŸ”Ή 7. Rule of Thumb

  • If using immutable state (string, int, tuple) β†’ No backtracking.
  • If using mutable state (list, array, dict, set) β†’ Backtracking required.

πŸ”Ή 8. Comparing Both Approaches

Approach Example Memory Undo Needed? When to Use
Immutable State Passing p + ch in strings High ❌ No When input size is small / immutables are used
Mutable State Modifying list with .append() Low βœ… Yes When input size is large / memory optimization needed

πŸ”Ή 9. Example: Subset Problem

Without Backtracking (immutable list copies)

def subsets(nums):
    ans = []
    def dfs(index, path):
        if index == len(nums):
            ans.append(path[:])   # copy (new list)
            return
        dfs(index+1, path + [nums[index]])   # new list created
        dfs(index+1, path)                   # no change
    dfs(0, [])
    return ans
Enter fullscreen mode Exit fullscreen mode

With Backtracking (mutable shared list)

def subsets(nums):
    ans = []
    def dfs(index, path):
        if index == len(nums):
            ans.append(path[:])   # copy final
            return
        path.append(nums[index])   # add
        dfs(index+1, path)
        path.pop()                 # undo
        dfs(index+1, path)
    dfs(0, [])
    return ans
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 10. Key Takeaways

  1. Recursion = breaking problem into smaller subproblems.
  2. Backtracking = recursion + undoing changes.
  3. Check mutability:
  • Immutable β†’ no backtracking.
  • Mutable β†’ backtracking needed.

    1. Trade-off:
  • Immutable β†’ simpler, more memory.

  • Mutable β†’ efficient, needs careful undo.


Top comments (0)