λDSA Learning Hubpart of DSA Atlas

Recursion

Intermediate~3h · 8 lessons10 practice problems

Functions that call themselves: base cases, the call stack, recursion trees, memoization, divide & conquer — and the discipline that stops infinite loops.

0 of 8 lessons checked off

Introduction

What it is

  • Recursion solves a problem by solving smaller copies of itself: a recursive case that shrinks the input, and a base case that stops the shrinking.
  • Every recursive call pushes a frame onto the call stack; the deepest chain of unfinished calls determines the space cost.

Why it matters

  • Trees, graphs, backtracking, divide & conquer, and dynamic programming are all naturally recursive — mastering recursion first makes those four topics feel like variations instead of new subjects.
  • Interviewers use recursion to test whether you can define a problem in terms of itself — the core skill behind writing correct DP transitions.

How it works

  • Write the base case FIRST and make it airtight. Then write the recursive case assuming the function already works on smaller inputs — the 'leap of faith' that makes recursion writable.
  • Trust the contract: never trace five levels deep while writing. Trace only to verify, and only on tiny inputs.
  • When the same subproblem recurs (fib(3) computed twice), cache results — memoization — and exponential trees collapse to linear work.

Where it's used

  • File-system walkers, JSON/AST parsers, React component trees, org-chart aggregation — anything nested processes naturally by recursion.

In interviews

  • All tree problems (height, LCA, path sums), permutations/subsets via backtracking, merge/quick sort, and the memoized recursions that become dynamic programming.
Analogy: Recursion is asking the person in front of you in a line 'what's your position?' — they ask the person ahead, until the front person answers '1' (base case), and answers flow back, each adding one.

Interactive diagram

Watch identical subproblems (f(2), f(1)) get recomputed — the overlap that memoization eliminates.

Naive fib(4) call tree

Each call spawns two more. Identical subproblems (same argument) are recomputed again and again — that overlap is what DP removes.

Lessons in this topic

Check off lessons as you go — your progress is saved in this browser.

  1. Recursive thinking: base + recursive case

    Shrink toward a guaranteed floor; the leap of faith.

    20 min
  2. The call stack

    Frames, unwinding, Python's ~1000 depth limit, stack overflow.

    15 min
  3. Recursion trees

    Drawing the calls to read off time complexity (branches^depth).

    20 min
  4. Tail recursion

    What it is, and why Python doesn't optimise it (rewrite as a loop).

    10 min
  5. Divide and conquer

    Split, solve halves, combine: merge sort as the template.

    25 min
  6. Memoization

    Caching overlapping subproblems: exponential → linear.

    25 min
  7. Backtracking introduction

    Choose → explore → un-choose; full topic later in the roadmap.

    20 min
  8. Common recursion mistakes

    Missing/wrong base case, non-shrinking input, shared mutable state.

    15 min

Operations

Anatomy of a recursive function

Base case first, then recurse on strictly smaller input, then combine. Factorial shows all three in four lines.

Anatomy of a recursive function
def factorial(n: int) -> int:    """n! for n >= 0. O(n) time, O(n) stack."""    if n < 0:        raise ValueError("factorial is undefined for negatives")    if n <= 1:              # base case: the guaranteed floor        return 1    return n * factorial(n - 1)   # recursive case: smaller inputdef list_sum(nums: list[int]) -> int:    """Sum via structural recursion — the shape used on trees."""    if not nums:            # base case: empty structure        return 0    return nums[0] + list_sum(nums[1:])   # note: slicing costs O(n)!
Time: factorial: O(n) — list_sum: O(n²) because each slice copiesSpace: O(n) call stack (plus slice copies in list_sum)

Edge cases

  • Validate inputs BEFORE recursing — one check at the top, not once per frame (or use an inner helper).
  • n = 0 and n = 1 both hit the base case here; check yours covers all floors.
  • Python's default recursion limit ≈ 1000 frames — deep inputs need iteration.

Common mistakes

  • Base case that can be stepped over (n == 1 when calls can reach 0).
  • Recursing on the same-size input — infinite recursion.
  • Hidden O(n) work per frame (slicing) silently squaring the complexity — pass indexes instead.

Memoization

Same recursion, plus a cache keyed by arguments. Each distinct subproblem is computed once; repeats are O(1) lookups.

fib(5) with memoization

Same recursion, but results are cached. Repeated subproblems become O(1) lookups, collapsing the tree to O(n) nodes.

from functools import lru_cachedef fib(n: int, memo: dict[int, int] | None = None) -> int:    """Manual memoization: O(n) time, O(n) space."""    if memo is None:        memo = {}    if n <= 1:        return n    if n in memo:        return memo[n]                 # cache hit — no subtree grows    memo[n] = fib(n - 1, memo) + fib(n - 2, memo)    return memo[n]@lru_cache(maxsize=None)               # the idiomatic versiondef fib_cached(n: int) -> int:    if n <= 1:        return n    return fib_cached(n - 1) + fib_cached(n - 2)
Time: O(n) memoized vs O(φⁿ) ≈ O(1.618ⁿ) naiveSpace: O(n) cache + O(n) stack

Edge cases

  • memo=None default, created inside — a mutable {} default would leak state across calls.
  • Cache key must capture ALL varying arguments (use tuples for multi-arg).
  • lru_cache requires hashable arguments — lists must become tuples.

Common mistakes

  • Memoizing a function whose subproblems never repeat (pure overhead).
  • Caching on mutable state that changes between calls, serving stale answers.

Divide and conquer (merge sort skeleton)

Split the input, recursively solve each half, combine the results. Complexity reads straight off the recurrence.

Divide and conquer (merge sort skeleton)
def merge_sort(nums: list[int]) -> list[int]:    """T(n) = 2T(n/2) + O(n)  →  O(n log n). Stable."""    if len(nums) <= 1:                 # base: already sorted        return nums    mid = len(nums) // 2    left = merge_sort(nums[:mid])      # divide + conquer    right = merge_sort(nums[mid:])    return _merge(left, right)         # combinedef _merge(left: list[int], right: list[int]) -> list[int]:    out: list[int] = []    i = j = 0    while i < len(left) and j < len(right):        if left[i] <= right[j]:            out.append(left[i]); i += 1        else:            out.append(right[j]); j += 1    out.extend(left[i:])    out.extend(right[j:])    return out
Time: O(n log n) — log n levels × O(n) merge work per levelSpace: O(n) buffers + O(log n) stack

Edge cases

  • Empty and single-element lists are the base case — they must return, not recurse.
  • <= in the merge keeps equal elements stable.
  • Leftover runs after one side empties must be appended.

Common mistakes

  • Splitting off zero elements (mid = 0 on len 1 without a base case) → infinite recursion.
  • Combining with repeated += string/list building instead of a linear merge.

Complexity analysis

OperationBestAverageWorstSpace
Linear recursion (factorial)O(n)O(n)O(n)O(n) stack
Binary recursion, no cache (naive fib)O(2ⁿ)O(2ⁿ)O(2ⁿ)O(n) stack
Memoized recursion (fib)O(n)O(n)O(n)O(n)
Divide & conquer (merge sort)O(n log n)O(n log n)O(n log n)O(n)
Backtracking (subsets)O(2ⁿ)O(2ⁿ)O(2ⁿ)O(n) stack

Recursion-tree reading: time ≈ number of nodes × work per node; space ≈ deepest path (plus any cache).

Python implementation

Production-quality code with type hints, validation, and docstrings.

One problem, three costs: naive vs memoized vs iterative fib
import timefrom functools import lru_cachedef fib_naive(n: int) -> int:    if n <= 1:        return n    return fib_naive(n - 1) + fib_naive(n - 2)      # O(1.618^n)@lru_cache(maxsize=None)def fib_memo(n: int) -> int:    if n <= 1:        return n    return fib_memo(n - 1) + fib_memo(n - 2)        # O(n), O(n) spacedef fib_iter(n: int) -> int:

What interviewers expect you to know

What interviewers expect you to know

  • Base case + strictly shrinking input = termination proof. Be able to say both for any function you write.
  • Stack space counts: 'O(h) for recursion' on trees, 'O(n)' on linear recursions.
  • Recursion-tree complexity: branches^depth nodes × per-node work; memoization prunes to distinct states.
  • Python has no tail-call optimisation — deep linear recursion should become a loop.

Classic follow-ups

  • "Convert it to iteration" — explicit stack for tree shapes; running pair of variables for linear shapes.
  • "How many distinct subproblems?" — the question that converts your recursion into DP sizing.
  • "What's the recursion depth on this input?" — checking you know when 10⁵ depth will crash Python.

How to talk through recursion

  • State the function's CONTRACT in one sentence before coding: 'height(node) returns the height of the subtree rooted at node.' Every recursive call is then justified by the contract, not by tracing.
  • Verify with the smallest inputs only: empty, one element. If those are right and the input shrinks, induction does the rest.

Common mistakes

Missing or unreachable base case

fib(n)==fib(n-1)+fib(n-2) with only `n == 0` as base loops forever on n=1. Cover every floor the recursion can land on.

Input that doesn't shrink

search(node) that recurses on the same node, or ranges that never narrow, recurse infinitely. Point to what strictly decreases each call.

Shared mutable state across branches

Appending to one list in both branches without undoing (or copying) makes sibling calls see each other's leftovers — the classic backtracking bug.

Hidden per-frame O(n)

nums[1:] copies the list every call: an 'O(n)' recursion becomes O(n²). Pass indexes, not slices.

Recursing before validating

Input checks inside the recursive path run n times (or worse). Validate once in a public wrapper, recurse in a private helper.

Trusting Python with 10⁵ depth

RecursionError at ~1000 frames. For deep linear structures (long linked lists, path-shaped trees), write the loop.

Practice problems

Hand-picked from the 355-problem DSA Atlas. Reveal hints and solutions progressively; mark and bookmark as you solve.

Easy (2)

Medium (6)

Combination Sum
MediumCombination search with unlimited reuse~25 min

Commonly associated with: Amazon, Meta, Airbnb, Uber

O(N^(T/M + 1)) where N = len(candidates), T = target, M = smallest candidate time · O(T/M) space

Hard (2)

Topic quiz

5 questions across concepts, code output, complexity, and scenarios. Answer all, then submit for explanations.

  1. Code output1. What happens when you call mystery(5)?
    def mystery(n):    if n == 0:        return 0    return n + mystery(n - 2)
  2. Complexity2. Naive fib(n) makes roughly how many calls?
  3. Concept3. Memoization helps exactly when…
  4. Complexity4. A recursive tree-height function on a balanced tree of n nodes uses how much stack space?
  5. Scenario5. Your recursive solution crashes with RecursionError on a 100,000-node linked list. Best fix?

Frequently asked questions

How do I stop mentally tracing every call?

Use the contract discipline: write one sentence for what f(x) returns, make the base case honour it, then write the recursive case USING the contract on smaller inputs. Trace only n=0,1 to verify. This is how experienced engineers write recursion quickly.

Is recursion slower than iteration?

In Python, moderately — function-call overhead is real, and there's no tail-call optimisation. But interviews grade Big-O and clarity first. Choose recursion where structure demands it (trees, backtracking); convert to loops when depth threatens the stack.

What's the difference between memoization and dynamic programming?

Memoization is top-down DP: recursion + cache, computing only needed states. Tabulation is bottom-up DP: loops filling a table in dependency order. Same subproblem graph, different traversal — the DP topic builds directly on this page.

Summary & cheat sheet

Key takeaways

  • Base case first, airtight, covering every reachable floor; input must strictly shrink.
  • State the contract, take the leap of faith, verify only tiny cases.
  • Time = recursion-tree nodes × per-node work; space = deepest path + cache.
  • Overlapping subproblems + cache = memoization; it's the front door to DP.
  • Python: ~1000-frame limit, no TCO — deep linear recursion becomes a loop.

Formulas & cheat sheet

  • Recursion-tree size ≈ branches^depth
  • T(n) = 2T(n/2) + O(n) → O(n log n); T(n) = T(n−1) + O(1) → O(n)
  • Memoized cost = distinct states × work per state

Interview checklist

  • I write the base case before the recursive case, every time.
  • I can state any recursive function's contract in one sentence.
  • I can draw the recursion tree and read off complexity.
  • I know when to memoize and when to convert to a loop.