Recursion and Backtracking Cheat Sheet for Coding Interviews

June 19, 20268 min read
dsaalgorithmsinterview-prepleetcode
Recursion and Backtracking Cheat Sheet for Coding Interviews
TL;DR
  • Every recursive function has three jobs: base case, recursive case, and return value — missing any one causes silent failures like returning None.
  • Recursion space equals stack depth, not total calls: O(n) for linear chains, O(log n) for halving, O(h) for tree DFS.
  • The backtracking template is choose-explore-unchoose: always copy state with path[:] on collection or every result points to the same mutable list.
  • Three tree shapes determine complexity before you write a line: subsets O(n·2^n), permutations O(n·n!), combinations O(k·C(n,k)).
  • Pruning slashes exponential runtimes: constraint, bound, and symmetry pruning each eliminate a different class of dead branches.
  • Never memoize backtracking: lru_cache caches return values but skips the side-effect mutations that build your result list.

Your interview is in 30 minutes. You don't need another tutorial. This recursion and backtracking cheat sheet covers the templates, complexity rules, pruning patterns, and bug traps that actually come up, with the exact facts you'll reach for under pressure.


Every Recursive Function Has Three Jobs

Base case, recursive case, and the return value. All three must be right, or nothing works.

def sum_to(n): if n == 0: # 1. base case: when you stop return 0 return sum_to(n - 1) + n # 2. recursive case: smaller subproblem # 3. return value: propagated back up

Before writing the first line, answer three questions: what is the smallest valid input (your base case), how does each call reduce the problem toward it, and what does one call return to its parent.

The most common bug: writing solve(n - 1) instead of return solve(n - 1). The recursive call runs, but the return value silently disappears. Your function returns None. The interviewer watches you stare at it for three minutes.


Recursion Space Is Depth, Not Total Calls

The call stack holds one frame per active call. Once a frame returns, it's gone. So space = O(maximum depth), not O(total calls).

PatternStack depthSpace
Linear chain (factorial, linked list)nO(n)
Halving each call (binary search)log nO(log n)
Tree DFS (height h)hO(h)
Backtracking (max depth d)dO(d)

Python's default limit is 1,000 frames. Hitting it raises RecursionError before the OS kills you. You can raise it with sys.setrecursionlimit(n), but in an interview, just acknowledge the risk for large n and offer to convert to iteration if the depth is unbounded. See recursion space complexity for the full breakdown.


The Master Theorem (and When You Don't Need It)

For divide-and-conquer, use the Master Theorem. For linear recursion, just count the levels.

The Master Theorem applies to T(n) = aT(n/b) + O(n^d):

Compare a vs b^dTime complexity
a < b^dO(n^d) (top-level work dominates)
a = b^dO(n^d · log n) (work spreads evenly across all levels)
a > b^dO(n^(log_b a)) (leaf work dominates)

Common results worth having cold:

AlgorithmRecurrenceTime
Binary searchT(n) = T(n/2) + O(1)O(log n)
Merge sortT(n) = 2T(n/2) + O(n)O(n log n)
FactorialT(n) = T(n-1) + O(1)O(n)
Fibonacci (naive)T(n) = T(n-1) + T(n-2)O(2^n)
Fibonacci (memoized)n states × O(1) eachO(n)

For T(n) = T(n-1) + O(1): n levels, O(1) each, total O(n). No theorem needed.


The Backtracking Template Is Three Operations

Choose, explore, unchoose. Every backtracking solution is a variation on this.

def backtrack(state, choices): if is_complete(state): result.append(state[:]) # copy, never reference return for choice in choices: if is_valid(choice, state): state.append(choice) # choose backtrack(state, next_choices) # explore state.pop() # unchoose

The state[:] copy on collection is not optional. If you append state directly, every entry in your result list points to the same mutable list. By the time the function finishes, they all show the empty final state. You run your solution, get back a list of empty lists, and have a small personal crisis. This is the single most common backtracking bug. More on backtracking bugs.

SMBC comic: "Problems with Recursion? Please take one", the flyer is a flyer about problems with recursion

The backtracking template is exactly this. If you forget the copy, you'll need one of these.


Every Backtracking Problem Has One of Three Tree Shapes

The shape determines the complexity before you write a line of code.

Subsets (include or skip each element)

Binary decision at each of n levels. Every possible subset is a leaf.

  • Time: O(n · 2^n): 2^n leaves, O(n) to copy each
  • Space: O(n): call stack depth equals n
def subsets(nums): result = [] def backtrack(start, path): result.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1, path) path.pop() backtrack(0, []) return result

Permutations (use each element once, order matters)

n choices at level 1, n-1 at level 2, and so on.

  • Time: O(n · n!): n! leaves, O(n) to copy each
  • Space: O(n)
def permute(nums): result = [] def backtrack(path, used): if len(path) == len(nums): result.append(path[:]) return for i, n in enumerate(nums): if not used[i]: used[i] = True path.append(n) backtrack(path, used) path.pop() used[i] = False backtrack([], [False] * len(nums)) return result

Combinations (choose k from n, order doesn't matter)

Choose k elements from n, moving only forward through the array.

  • Time: O(k · C(n, k)): C(n,k) leaves, O(k) to copy each
  • Space: O(k)

Which shape is it? If order matters, permutations. If you pick exactly k items, combinations. If each element is in or out, subsets.


Pruning Kills Dead Branches Early

Adding one validity check before recursing can reduce runtime from minutes to milliseconds on hard inputs.

Three categories:

  1. Constraint pruning. The partial state already violates a hard rule. Skip the whole subtree. In N-Queens, check column and diagonal conflicts before placing.

  2. Bound pruning. Even the best possible completion from here can't beat the current best solution. Used in optimization variants like word search or TSP approximations.

  3. Symmetry pruning. Two sibling choices produce the same result. Take only one. In Subsets II with duplicates, skip any nums[i] == nums[i-1] where i > start.

# Symmetry pruning for duplicates (sort nums first) for i in range(start, len(nums)): if i > start and nums[i] == nums[i - 1]: continue path.append(nums[i]) backtrack(i + 1, path) path.pop()

The sort before the call is not optional. It makes equal elements adjacent, which is what the duplicate check depends on. See pruning in backtracking for the full taxonomy.


Tree DFS: Process Order Is Everything

Most tree problems are depth-first search with a choice of where to process each node.

def dfs(node): if not node: return # preorder: process here (parent before children) dfs(node.left) # inorder: process here (left, root, right) dfs(node.right) # postorder: process here (children before parent)

When to use each:

OrderUse when
PreorderBuilding a copy, serialization, prefix-style problems
InorderExtracting BST elements in sorted order
PostorderParent value depends on children (height, diameter, path sums from leaves)

Memoization Turns Recursion Into DP

If the same inputs always produce the same output and the same inputs repeat, cache the result.

from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)

Time after memoization = (number of distinct states) × (work per state, not counting recursive calls).

Fibonacci: n states × O(1) work = O(n). Grid paths: m×n states × O(1) work = O(m·n). Coin change with k denominations and amount A: A×k states × O(1) work = O(A·k).

Do not memoize backtracking functions. Backtracking works by mutating state and undoing it. The function has side effects. lru_cache caches return values, not side effects. The cache returns the right value while silently skipping the mutation that builds your result list. Your output is empty and you have no idea why. You've been warned.


Bugs That Pass Tests and Fail Interviews

Run this checklist before you say you're done.

  • Missing base case. What happens at empty input, single element, or n=0? Each deserves a mental trace.
  • Off-by-one base case. Returns early by one level, or one too late.
  • Aliasing. Appending path instead of path[:] into the result list. Your result is a list of identical empty lists. Congratulations.
  • Dropped return. solve(n - 1) computes the answer and throws it away. The function returns None. This is the bug that looks correct for thirty seconds.
  • Modifying the input array. The calling frame still depends on the original.
  • Stack overflow. Linear recursion on n = 10^5 crashes Python at depth 1,000. Note it and offer iteration.

Michael Scott shocked, Programmers when they finally get a different error message

The energy when you fix the dropped return and something else breaks. Progress.


Recursion and Backtracking Complexity Reference

Problem typeTimeSpace
SubsetsO(n · 2^n)O(n)
PermutationsO(n · n!)O(n)
Combinations C(n,k)O(k · C(n,k))O(k)
Memoized recursionO(states × work)O(states + depth)
Tree DFSO(n)O(h)
Binary search (recursive)O(log n)O(log n)
Merge sortO(n log n)O(n)

Reading a reference helps. Saying it out loud under interview conditions is what actually transfers. SpaceComplexity runs voice-based mock interviews on backtracking and recursion problems with rubric-based feedback on your communication, not just your code.


Further Reading