๐Ÿช† Recursion

A standalone course โ€” stop tracing, define a contract and trust it. Built to take you from "I freeze on recursion" to writing it cold in an SDE2/SDE3 interview.

This is a standalone course on recursion โ€” built to take you from "I freeze on recursion" to writing it cold in FAANG / SDE2โ€“SDE3 interviews. It assumes nothing from any other sheet; everything you need is here. The core belief: recursion feels hard only because people try to trace it โ€” following every call in their head until they're dizzy. Strong engineers never trace. They define a contract and trust it (the Leap of Faith, ยง1), then verify just one level. Before you write a single line, name WHICH of the 5 archetypes (ยง2) the problem is โ€” that one classification decision does more for you in an interview than any amount of tracing. Return values are the thing you said trips you up, so they get the most attention: ยง3's live derivations, the two โญ sections ยง5โ€“ยง6, and the diagrams in ยง11 and ยง14.
HOW TO USE THIS DOCUMENT โ€” a learning path (don't just skim topโ†’bottom)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  PHASE 1 ยท UNDERSTAND   ยง1โ€“ยง8    the mindset, the 5 archetypes, the 4
                                  questions, and the two โญ return-value
                                  sections. Read slowly, ONCE, with a pen.
                                  Do the live derivations in ยง3.

  PHASE 2 ยท PATTERNS     ยง9โ€“ยง15   the handful of shapes every recursive
                                  solution takes. Try to write each one
                                  yourself BEFORE reading the code.

  PHASE 3 ยท APPLY TO DSA ยง16โ€“ยง17  how recursion BECOMES dp, backtracking,
                          + ยง20   divide & conquer, and tree/graph search โ€”
                                  the actual interview topics.

  PHASE 4 ยท DRILL        ยง18โ€“ยง19  bug patterns, the keywordโ†’approach map,
                          + ยง21   and the LC coverage table โ€” then the
                                  self-test (active recall) to prove it.

  GOLDEN HABIT: cover the code, predict the base case + the combine line,
  THEN look. Recursion is learned by producing it, not by reading it.

1. THE RECURSION MINDSET

๐Ÿ”‘ The one idea that unlocks everything โ€” the Leap of Faith: Assume your function already works for any smaller input. Don't look inside it. Your only job is to handle one level: turn the answer for the smaller input into the answer for the current input.
WHAT RECURSION ACTUALLY IS
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Solve a problem by solving a SMALLER version of the SAME
problem, then doing a tiny bit of work to combine.

You write ONE level. The machine repeats it for you.

THE LINE ANALOGY (burn this into memory)
You are standing in a line and want your position.
  โ€ข You DON'T count everyone (that's iteration / tracing).
  โ€ข You tap the person in front: "what's your number?"
  โ€ข You trust their answer X, and reply  X + 1.
  โ€ข The FIRST person has nobody ahead โ†’ answers "1".  (BASE CASE)
That is recursion: trust the smaller answer, add your bit.

THREE MENTAL MOVES
  1. Shrink   โ€” make the input smaller (n-1, n/2, node.left)
  2. Trust    โ€” assume the recursive call returns the right thing
  3. Combine  โ€” do O(1)-ish work to build your answer from it
WAY DOWN shrink n โ†’ n-1 WAY UP combine & return fact(4) fact(3) fact(2) fact(1) = 1 BASE CASE โ€” no recursion โ†‘ 4 ร— 6 = 24 โ†‘ 3 ร— 2 = 6 โ†‘ 2 ร— 1 = 2 โ†‘ returns 1
The Leap of Faith, visualized. Calls shrink n on the way DOWN to the base case; the answer is built by combining (ร—) on the way UP. You only ever write ONE level โ€” n ร— fact(n-1) โ€” and trust the rest.
โš ๏ธ Stop tracing. If you ever find yourself drawing the 4th level of a call tree to "see if it works," you've left the recursive mindset. Verify only two things: (a) the base case is correct, and (b) one recursive step is correct assuming the call below it is correct. Induction does the rest โ€” for free.

2. โญ THE 5 ARCHETYPES OF RECURSION โ€” name the conversation before you code

Every recursive problem is one of 5 conversations between a call and its children. Name it in 10 seconds, before Q0โ€“Q3 โ€” it decides the shape of everything you write next.

๐Ÿ”‘ The move that separates senior engineers from someone who's memorized templates: before you touch the 4-question framework (ยง3) or pick a shape (ยง9), ask "which of these 5 conversations is my function having with its children?" That single classification decision is why a senior engineer stares at a new problem for 20 seconds and then just writes it, while everyone else starts typing and gets lost halfway through. It's the missing layer above "how do I write the code" โ€” it's "what KIND of thing am I even building."
THE QUESTION BEHIND THE QUESTION: "who ends up holding the final answer,
and how did it get there?" Five different answers, five archetypes:

  1. BUBBLE-UP        "Child, what's YOUR answer?" I trust it, I combine
     (return-value)    it with my bit, I hand MY answer up to MY parent.

  2. CARRY-DOWN        "Here's what you need to know." I hand context
     (state-passing)   DOWN so my children don't have to re-derive it.

  3. GLOBAL-ANSWER     "Everyone write to the same whiteboard." No single
     (shared/closure)  return value holds the final answer โ€” every frame
                        just might update one shared slot as it passes by.

  4. EXPLORE-ALL       "Try every choice, then take it back." Loop over
     (backtracking)    every option, recurse, undo โ€” build the full
                        search space instead of one answer.

  5. REMEMBER          "Have I already answered THIS exact question?"
     (memo โ†’ DP)        Same recursion as #1, plus a cache, because the
                        same sub-question keeps getting asked.
#ArchetypeInfo flowSignal phrases in the problemMechanics / sheet
1Bubble-upUP only (return)"compute the X of the whole structure" โ€” sum, height, count, does-a-path-existยง5 Style A
2Carry-downDOWN (parameters)index / remaining budget / path-so-far belongs in the signature; "process in order"ยง5 Style B, ยง14
3Global-answerSIDEWAYS (closure / outer var)"find the best/max/min over the WHOLE tree", where the winner can appear anywhere, not just at the rootยง14 closure channel
4Explore-allDOWN + undo"all subsets/combinations/permutations", "find every way toโ€ฆ", "list them"multi-branch shape ยง9 โ†’ backtracking.html
5Remember (memo)UP + cache"count the number of ways", "min/max cost to reach", the same (n, i, โ€ฆ) keeps recurringยง16 โ†’ dp.html
#1 vs #2 โ€” you already know these; they're ยง5's Style A / Style B, just renamed as "conversations" instead of "styles." The new, higher-leverage ideas here are #3, #4, and #5 โ€” worked below.

Archetype 3, worked โ€” diameter of a binary tree (LC 543)

โš ๏ธ The trap that fools people who think they understand bubble-up: this function DOES have a return statement, so it LOOKS like archetype 1. But look at what's returned: it's the height, not the diameter. The diameter โ€” the actual answer to the problem โ€” is never returned by anyone. It's written to a shared variable that every frame is allowed to update as it passes through.
function diameterOfBinaryTree(root: TreeNode | null): number {
  let best = 0;                                    // the SHARED WHITEBOARD โ€” the real answer lives here
  function height(node: TreeNode | null): number {  // this still returns something โ€” but NOT the final answer
    if (node === null) return 0;
    const L = height(node.left);
    const R = height(node.right);
    best = Math.max(best, L + R);                  // side-channel write: "the best path THROUGH me"
    return 1 + Math.max(L, R);                      // plumbing: what MY PARENT needs to keep computing heights
  }
  height(root);
  return best;                                       // the real answer, read back out at the end
}
๐Ÿ”‘ Why you need this archetype at all: the winning path can be centered at ANY node, not just the root. A pure bubble-up return can only tell you "the best answer within my subtree" โ€” that's a different question than "the best answer seen anywhere so far." Rather than thread a second value through every single return in the tree, you park it in one shared slot everyone can touch. Nuance for the interview: this is a CHOICE, not a law of physics โ€” you could instead return a [height, bestSoFar] tuple and bubble both up (pure archetype 1). Global-answer is simply less bookkeeping. Naming both options out loud is a senior move.

Archetype 4, worked โ€” explore every subset (LC 78)

The tell: the problem doesn't want ONE answer, it wants EVERY answer. That forces a different contract: instead of "return the answer," it's "for each choice, recurse into it, then undo the choice so my sibling starts clean." Full mechanics, pruning, and 10+ templates live in backtracking.html โ€” this is just the recursive skeleton underneath.
function subsets(nums: number[]): number[][] {
  const res: number[][] = [];
  const path: number[] = [];
  function dfs(i: number): void {
    if (i === nums.length) { res.push([...path]); return; }  // base: record ONE complete answer
    path.push(nums[i]); dfs(i + 1); path.pop();                // choice A: take nums[i] โ€ฆ then UNDO
    dfs(i + 1);                                                  // choice B: skip nums[i]
  }
  dfs(0);
  return res;
}

Archetype 5, worked โ€” same recursion, one line added

The tell: archetype 1 (bubble-up), but the same (n) or (i, j, remaining) tuple gets asked more than once as the tree unfolds. You don't redesign the recursion โ€” you wrap it with a cache. Full derivation, the recursion tree, and the top-downโ†’bottom-up conversion live in ยง16.
Same function, ONE line added:
  archetype 1:  return f(n-1) + f(n-2);
  archetype 5:  if (memo.has(n)) return memo.get(n);
                const ans = f(n-1) + f(n-2);
                memo.set(n, ans); return ans;
๐Ÿ”‘ The 10-second classifier โ€” run through in order, stop at the first "yes":
Does the same (n) / (i,j) tuple get asked more than once?
   โ†’ YES: archetype 5, REMEMBER (memo). Design it as #1 first, then cache.

Does the problem say "all / every / list them / how many WAYS
(as distinct sequences, not just a count)"?
   โ†’ YES: archetype 4, EXPLORE-ALL (backtracking).

Can the final answer live ANYWHERE in the tree, not just be
computable from the root's own return value?
   โ†’ YES: archetype 3, GLOBAL-ANSWER (closure/shared var).

Does a child need to know something only an ancestor has seen
(an index, a running sum, a path-so-far)?
   โ†’ YES: archetype 2, CARRY-DOWN (state-passing).

Otherwise:
   โ†’ archetype 1, BUBBLE-UP (return-value). This is the default โ€”
     most problems are this one. Reach for the others only when
     bubble-up alone can't say what you need.
๐ŸŽค Interview lens What they're really testing: not whether you can write any of these five โ€” eventually, by trial and error, you would. It's whether you can say the archetype out loud in the first 30 seconds, before writing a line of code. Compare "let me think out loud: this wants every combination, so I'll explore-all with an undo" against 4 minutes of silent typing and backspacing โ€” same destination, very different signal. ยท Escalation: interviewers love mutating one archetype into another mid-interview. Turn a bubble-up sum into a memoized count by adding overlapping subproblems. Turn a global-answer diameter into a return-value tuple by removing the closure. Converting between archetypes on request proves you understand them โ€” not just memorized them.

3. THE 4-QUESTION FRAMEWORK

Answer these 4 questions, in order, and the code writes itself. Do them on paper before touching the keyboard. Most "I can't think recursively" moments are really "I skipped Q0".
Q0. CONTRACT  โ€” "What does f(x) return / do, in ONE sentence?"
    Write it as a precise English sentence FIRST. Everything
    else is derived from this. If you can't say it, you can't
    code it.
      e.g. "height(node) = number of nodes on the longest
            path from node down to a leaf."

Q1. BASE CASE โ€” "What is the smallest input I can answer
    WITHOUT recursing?" Return that answer directly.
      e.g. height(null) = 0.

Q2. RECURSIVE CASE (the leap) โ€” "If f already works for the
    smaller input, how do I build f(x) from it?"
      e.g. height(node) = 1 + max(height(left), height(right)).

Q3. PROGRESS โ€” "Does every call move strictly toward the
    base case?" (smaller n, smaller subtree, higher indexโ€ฆ)
    If not โ†’ infinite recursion / stack overflow.
// The 4 questions, mapped onto code, for height-of-tree:
class TreeNode { val = 0; left: TreeNode | null = null; right: TreeNode | null = null; }

function height(node: TreeNode | null): number {   // Q0: returns # nodes on longest rootโ†’leaf path
  if (node === null) return 0;                      // Q1: base case, answered directly
  const L = height(node.left);                      // Q2: TRUST these return correct heights
  const R = height(node.right);                     //     (the leap of faith โ€” don't trace in)
  return 1 + Math.max(L, R);                        // Q2: combine into MY answer
  // Q3: node.left / node.right are strictly smaller โ†’ progress guaranteed
}
Q0 ยท CONTRACT "f(x) returns ___ , meaning ___" (one sentence) โ†’ fixes the return TYPE Q1 ยท BASE CASE smallest input โ†’ its answer (the identity value) โ†’ the starting value Q2 ยท RECURSIVE LEAP assume f(smaller) works โ†’ combine into my answer โ†’ the recursive line Q3 ยท PROGRESS does the input strictly shrink every call? โ†’ no infinite recursion Answer top โ†’ bottom on paper before you type. Seniors spend 80% of their time on Q0.
The framework as a pipeline. Each question produces one piece of the code. Skip Q0 and the other three have nothing to stand on โ€” that's the "I can't think recursively" feeling.
Why this order matters. Q0 fixes the meaning and the return type. Q1 gives you the value to start from. Q2 is the only "creative" step, and it's easy once Q0 is precise. Q3 is your safety check. Beginners jump straight to Q2 and flail; seniors spend 80% of their time on Q0.

โ–ถ Apply the framework LIVE โ€” derive three from scratch

๐Ÿ”‘ This is the most important part of the document for you. Watch the 4 questions turn a blank screen into working code. Cover the code block and try it yourself first โ€” predict the base case and the combine line, then look. Notice the rhythm repeats every single time.
DERIVATION 1 โ€” sum of an array          (shape: LINEAR, 1 call)
  Q0 CONTRACT : sum(a, i) returns the sum of a[i], a[i+1], โ€ฆ, last.
  Q1 BY HAND  : sum of nothing (i past the end) = 0.
                sum([7], i=0) = 7 + (sum of nothing) = 7. โœ“
                โ†’ base case = "i reached the end" โ†’ return 0.
  Q2 LEAP     : TRUST sum(a, i+1) = "sum of the rest". Then
                sum(a, i) = a[i] + sum(a, i+1).
  Q3 PROGRESS : i grows by 1 each call โ†’ always reaches a.length. โœ“
function sum(a: number[], i = 0): number {
  if (i === a.length) return 0;       // Q1 โ€” base case
  return a[i] + sum(a, i + 1);        // Q2 โ€” my element + trusted sum of the rest
}
DERIVATION 2 โ€” height of a binary tree   (shape: TREE, 2 calls)
  Q0 CONTRACT : depth(node) returns the # of nodes on the longest
                path from node down to a leaf.
  Q1 BY HAND  : depth(null) = 0 (no nodes).
                depth(one leaf) = 1 + max(0, 0) = 1. โœ“
                โ†’ base case = "node is null" โ†’ return 0.
  Q2 LEAP     : TRUST depth(left) and depth(right). Then
                depth(node) = 1 + max(depth(left), depth(right)).
  Q3 PROGRESS : each child is a strictly smaller subtree โ†’ hits null. โœ“
function depth(node: TreeNode | null): number {
  if (node === null) return 0;                                 // Q1
  return 1 + Math.max(depth(node.left), depth(node.right));    // Q2
}
DERIVATION 3 โ€” climb stairs, 1 or 2 at a time  (shape: BRANCHING โ†’ DP)
  Q0 CONTRACT : ways(n) returns the # of distinct ways to climb n steps.
  Q1 BY HAND  : ways(0) = 1 (one way: you're already at the top).
                ways(1) = 1.   ways(2) = ways(1) + ways(0) = 2. โœ“
                โ†’ base cases = n===0 โ†’ 1,  n<0 โ†’ 0 (overshot the top).
  Q2 LEAP     : my last move was a 1-step OR a 2-step. TRUST both:
                ways(n) = ways(n-1) + ways(n-2).
  Q3 PROGRESS : n shrinks each call โ†’ reaches 0 / negative. โœ“
  NOTE        : the same ways(k) is recomputed many times โ†’ add a memo
                and this IS the classic DP, LC 70. (see ยง16)
function ways(n: number): number {
  if (n < 0) return 0;                  // Q1 โ€” overshot
  if (n === 0) return 1;                // Q1 โ€” reached the top
  return ways(n - 1) + ways(n - 2);     // Q2 โ€” sum the two trusted universes
}
See the rhythm? Contract โ†’ solve the tiniest cases by hand โ†’ the base case falls out of the tiniest case โ†’ the combine line is always "(my current element / node / last move) ๏ผ‹ (the trusted smaller answer)." Every recursion you will ever write in an interview follows this exact rhythm. Drill these three until you can reproduce each in under a minute.

4. UNIVERSAL TEMPLATE

// Memorize this skeleton. Every recursion is a fill-in-the-blanks of it.
function f(input: Input): Output {
  // 1) BASE CASE(S): smallest inputs, answered without recursion
  if (/* smallest case */) return /* known answer */;

  // 2) (optional) PREPARE: shrink the problem / pick the choices

  // 3) RECURSE: call f on STRICTLY SMALLER input(s) โ€” TRUST the result
  const sub = f(/* smaller input */);

  // 4) COMBINE: do O(1)-ish work to turn sub-answer(s) into MY answer
  return /* combine(sub, current-element) */;
}
THE FOUR SLOTS โ€” fill these in and you're done
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ SLOT       โ”‚ QUESTION TO ASK YOURSELF                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Base case  โ”‚ Smallest input? What's its answer?           โ”‚
โ”‚ Shrink     โ”‚ How do I make the input smaller? (n-1, n/2)  โ”‚
โ”‚ Recurse    โ”‚ Call f on the smaller thing. TRUST it.       โ”‚
โ”‚ Combine    โ”‚ child-answer  +  my-one-step-of-work  = ans  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

5. โญ RETURN-VALUE vs VOID โ€” THE BIG FORK

This is the thing you said trips you up, so read it twice. There are exactly two ways a recursion can produce an answer. Almost all confusion about "what do I return" comes from mixing them. Pick ONE style per function.
๐Ÿ”‘ The load-bearing analogy โ€” a report vs. a relay baton. Style A (bubble-up) is you reporting to your manager: you ask your one direct report a single question, wait for their one-number answer, add your own bit, and hand ONE number to your manager. Nobody above you ever sees anyone else's raw numbers โ€” just yours, already combined. Style B (carry-down) is a relay race: the baton (your running total, your path-so-far) gets handed FORWARD, runner to runner. Nothing meaningful happens on the way back โ€” the race is already decided the moment the last runner crosses the line holding the baton. If you can say which of these two your function is doing, you already know what to return.
Style A โ€” Bubble Up (report to manager)Style B โ€” Carry Down (relay baton)
Answer flowUP the stack: child returns โ†’ parent combines โ†’ parent returns up.DOWN the stack: parent hands the partial answer to the child as an argument.
ReturnsA meaningful value (number, list, bool, node).Often void โ€” pushes the finished answer into an external array once it reaches the base case.
Use whenYou can write f(x) as a formula over f(smaller).You're enumerating / building every path or combo, or you need the FULL path at the leaf.
Examplessum, height, count, fib, "does a path exist?" (bool), reverse-a-list, mergesort.print all paths, subsets, permutations, tree rootโ†’leaf paths, DFS that records routes.
Mantra"Return child-work, combine, return up.""Push state down, collect at the base case."
STYLE A ยท BUBBLE UP STYLE B ยท CARRY DOWN sumUp(a, 0) sumUp(a, 1) sumUp(a, 2) sumUp(a, 3) = 0 shrink i โ†‘ returns 0 โ†‘ 2 + 0 = 2 โ†‘ 5 + 2 = 7 โ†‘ 3 + 7 = 10 the SUM is computed on the way UP โ€” each frame does real work in the return line sumDown(a, 0, acc=0) sumDown(a, 1, acc=3) sumDown(a, 2, acc=8) sumDown(a, 3, acc=10) = 10 acc grows on the way down just plumbing โ€” no work the SUM is already final at the base case โ€” the way back up carries it, unchanged
Same problem, both styles, same numbers. Left: each frame does its real work AFTER the recursive call returns (on the way up) โ€” that's the report-to-your-manager shape. Right: each frame does its real work BEFORE recursing (on the way down); the base case already holds the answer, and the return trip back to the top is just handing it back unchanged โ€” that's the relay-baton shape.
// SAME PROBLEM, BOTH STYLES โ€” sum of an array from index i onward.

// STYLE A โ€” BUBBLE UP: the value comes back through `return`.
function sumUp(a: number[], i: number): number {
  if (i === a.length) return 0;            // base: empty tail sums to 0
  return a[i] + sumUp(a, i + 1);           // my element + (trusted) sum of the rest
}

// STYLE B โ€” CARRY DOWN: the running total rides in a parameter (accumulator).
function sumDown(a: number[], i: number, acc: number): number {
  if (i === a.length) return acc;          // base: the answer is whatever we accumulated
  return sumDown(a, i + 1, acc + a[i]);    // add now, pass forward; nothing to combine on the way up
}
// sumUp(a,0) and sumDown(a,0,0) give the same number.
// Notice: in A the work happens AFTER the call (on the way up);
//         in B the work happens BEFORE the call (on the way down).
๐Ÿ”‘ The decision in 5 seconds: Can I write the answer as combine(myElement, f(rest))? โ†’ Style A (return). Do I instead need to remember the whole path so far to record it at the end? โ†’ Style B (carry down + collect). Backtracking is Style B with an undo step โ€” see the Backtracking sheet.

6. โญ HOW TO FIGURE OUT THE RETURN VALUE

A repeatable 3-step recipe for "I don't know what to return." Run this every time and you'll never be stuck on a return value again.
๐Ÿ”‘ The analogy โ€” you're signing a contract before you do any work. A recruiter won't let you start a job without agreeing what you're being paid to deliver. Step 1 is you writing that one-sentence job description for yourself ("this function's job is to return ___"). Once it's written down, Steps 2 and 3 are just honoring that contract for the easiest case and then the general case โ€” you're not inventing anything new, you're fulfilling a promise you already made in Step 1.
STEP 1 ยท NAME THE CONTRACT "count(node) returns the number of nodes in this subtree." STEP 2 ยท SMALLEST INPUT "count(null) = 0" โ€” an empty subtree has 0 nodes. STEP 3 ยท THE LEAP "trust count(left) and count(right) โ†’ 1 + count(left) + count(right)" function count(node) { if (!node) return 0; // Step 2 return 1 + count(node.left) + count(node.right); // Step 3 } Each step above becomes exactly one line of the code below it.
The 3-step recipe, worked end to end. Name the contract first โ€” everything else is just honoring it. Step 2 becomes your if line; Step 3 becomes your return line. That's the whole function.
STEP 1 โ€” NAME THE RETURN (write the contract sentence).
   "f(x) returns ______."  Be specific about type AND meaning.
   The type you name here MUST be the same in every branch.
     number? list? boolean? a tree node? a tuple?

STEP 2 โ€” RETURN FOR THE SMALLEST INPUT (base case value).
   Plug the tiniest input into your sentence. What's the answer?
   This pins down the return TYPE and the identity/empty value.
     sum of []      โ†’ 0          (additive identity)
     product of []  โ†’ 1          (multiplicative identity)
     longest path of null โ†’ 0
     "found target in empty list?" โ†’ false
     reverse of "" โ†’ ""

STEP 3 โ€” RETURN FOR THE GENERAL INPUT (the leap).
   Say out loud: "I have the child's answer. I trust it.
   What ONE operation turns it into MY answer?"
   That sentence IS your recursive return line.
     count(node)   = 1 + count(left) + count(right)
     max(node)     = max(node.val, max(left), max(right))
     exists(x,i)   = a[i]===x || exists(x,i+1)

GOLDEN RULE: base case and recursive case must return the
SAME TYPE. If base returns a number, recursion returns a number.
Type-mismatch between branches = your bug.

Fill-in-the-blanks table โ€” copy this shape for any new problem.

ProblemBase returnRecursive return (combine)
length of list01 + f(rest)
sum of list0head + f(rest)
max of lista[i]max(a[i], f(i+1))
n!1n * f(n-1)
a^n1a * f(n-1)
contains x?falsea[i]===x || f(i+1)
all positive?truea[i]>0 && f(i+1)
reverse string""f(rest) + s[0]
tree node count0 (null)1 + f(left) + f(right)
tree height0 (null)1 + max(f(left), f(right))
tree contains xfalsenode.val===x || f(L) || f(R)
The pattern you should now SEE: the base return is always the identity element of whatever operation combines the answers โ€” the value that changes nothing when combined. +'s identity is 0, *'s is 1, ||'s is false, &&'s is true, string concat's is "". Some operations (like max/min) have no identity element at all โ€” for those, stop one step earlier and let the base case be the LAST single element instead of an empty input.
Trick for boolean returns: use || ("does ANY branch succeed?") or && ("do ALL branches succeed?"). Short-circuiting also prunes for free. Trick for "return the node/path itself": the return type is the node/list, base returns null / [], and you reattach the child's returned node โ€” see reverse-linked-list and tree-build problems.

7. FINDING THE BASE CASE

Rule: the base case is the input so small that recursing would make it illegal or trivial. Find it by asking "what's the smallest input my recursive line could be handed?" โ€” then make sure that input never reaches the recursive line.
WHERE BASE CASES LIVE (by input type)
  array / index  โ†’ i === arr.length   (ran off the end)   โ†’ identity
                 โ†’ i > j              (empty window)
                 โ†’ low > high         (binary search miss)
  number n       โ†’ n === 0  or  n === 1
  string         โ†’ s.length === 0  or  start === s.length
  tree node      โ†’ node === null      (most common!)
                 โ†’ node.left === null && node.right === null  (leaf)
  linked list    โ†’ head === null  or  head.next === null
  2D grid        โ†’ out of bounds, or cell already visited

TWO BASE-CASE STYLES FOR TREES (know both)
  A) Guard on null:   if (node === null) return 0;
     โ†’ simplest, handles empty tree, lets you recurse blindly.
  B) Guard on leaf:   if (!node.left && !node.right) return node.val;
     โ†’ needed when "leaf" is special (e.g. root-to-leaf sums).
     โ†’ DANGER: must also handle a node with ONE child, or you
       recurse into null and crash. Prefer style A when unsure.

CHECKLIST
  โ˜‘ Does the base case return the SAME TYPE as the recursive case?
  โ˜‘ Is it reachable? (does progress actually shrink toward it?)
  โ˜‘ Did I cover EVERY tiny input? (empty AND single-element)
  โ˜‘ For two-branch recursion, can a child be null/empty?
โœ“ GOOD โ€” step โˆ’1 always lands on the base f(4) f(3) f(2) f(1) f(0) BASE โœ— BAD โ€” step โˆ’2 from odd n skips 0 forever (==) f(5) f(3) f(1) f(-1) โˆž ๐Ÿ’ฅ
"Progress" means the argument moves strictly toward the base every call. When a step can jump past the base (here โˆ’2 misses 0), an === base test never fires โ†’ stack overflow. Fix: test with <= / >= so you catch the overshoot.
// Missing/weak base case is the #1 cause of stack overflow.
// BAD: only stops at exactly length โ†’ negative or skipped index loops forever
function bad(a: number[], i: number): number {
  if (i === a.length) return 0;
  return bad(a, i + 2);          // if length is odd, i jumps PAST length โ†’ never equal โ†’ ๐Ÿ’ฅ
}
// GOOD: use >= so you can never "overshoot" the base case
function good(a: number[], i: number): number {
  if (i >= a.length) return 0;  // catches i === length AND i === length+1
  return good(a, i + 2);
}

8. THE CALL STACK & ORDER OF WORK

Pre vs Post: code before the recursive call runs on the way DOWN (topโ†’base). Code after it runs on the way UP (baseโ†’top). Swapping those two lines is the difference between printing 1โ†’n and nโ†’1. Internalize this and half of "recursion magic" disappears.
function down(n: number): void {   // work BEFORE recurse โ†’ executes on the way DOWN
  if (n === 0) return;
  console.log(n);                  // prints n, n-1, ... 1   (5 4 3 2 1)
  down(n - 1);
}
function up(n: number): void {     // work AFTER recurse โ†’ executes on the way UP
  if (n === 0) return;
  up(n - 1);
  console.log(n);                  // prints 1, 2, ... n     (1 2 3 4 5)
}
THE CALL STACK for down(3) โ€” Last In, First Out down(0) ยท base down(1) down(2) down(3) โ†‘ bottom = first called & last to finish top = base case = first to finish PUSH on each call POP on each return
Max stack height = recursion depth = your SPACE cost. Frames pile up until the base case, then unwind in reverse order. Code before the call runs while pushing (down); code after runs while popping (up).
CALL STACK FOR down(3) โ€” frames PUSH going down, POP coming up
  call down(3)  โ”€โ” print 3
    call down(2) โ”‚  print 2
      call down(1)   print 1
        call down(0) base โ†’ return
      return โ—„โ”€โ”€โ”€โ”€ unwinding (nothing prints in `down`)
    return โ—„โ”€โ”€โ”€โ”€
  return โ—„โ”€โ”€โ”€โ”€
The stack's MAX HEIGHT = recursion depth = your SPACE cost.

THE THREE TRAVERSAL ORDERS ARE JUST "WHEN DO I VISIT ME?"
  preorder:   visit(me); recurse(L); recurse(R);   // me first (down)
  inorder:    recurse(L); visit(me); recurse(R);   // me in middle (BSTโ†’sorted)
  postorder:  recurse(L); recurse(R); visit(me);   // me last (up) โ€” needed when
                                                   //   I depend on my children's results
PREORDER INORDER POSTORDER me ยท L ยท R L ยท me ยท R L ยท R ยท me 1 2 3 2 1 3 3 1 2 visit me on the way DOWN visit me LAST โ€” children first The number = WHEN this node is visited. If MY answer needs my children's answers (height, sum, "is balanced?"), you must use POST-order โ†’ do the work after the calls.
Same tree, three visit orders โ€” it's just "when do I process me?" Before the calls = pre-order (top-down). Between = in-order (BST โ†’ sorted). After = post-order (bottom-up), the one you need whenever your return value combines your children's.
How to choose: if MY answer needs my children's answers (height, sum, "is balanced?"), you MUST do the work after the calls โ†’ post-order / bubble up. If I just need info from my ancestors (depth, path-so-far), pass it down as a parameter โ†’ pre-order / carry down.

9. THE 6 RECURSION SHAPES

Shape# callsSignature feelingTypical cost
1 Linear1 callf(n-1) once โ€” factorial, sum, list walkO(n) time, O(n) stack
2 Tail1 callf(...) is the LAST thing; answer fully in the paramO(n) time, O(1)* stack (*if engine optimizes)
3 Binary / Tree2 callsf(left) and f(right) โ€” fib, tree traversalsoften O(2โฟ) or O(n)
4 Multi-branchk callsa loop of recursive calls (one per choice)O(k^depth) โ€” subsets, permutations, n-ary trees
5 Divide & Conquer2 of n/2split, solve halves, merge โ€” mergesort, quicksort, bsearchT(n) = 2T(n/2) + f(n)
6 Mutual / Nestedvia otherf calls g, g calls f; or f(f(x)) (Ackermann, parsers)varies
How to recognize which one: one smaller input โ†’ Linear/Tail. Two children โ†’ Tree. "For each choice, recurse" โ†’ Multi-branch (this is backtracking). "Split in half" โ†’ Divide & Conquer. Grammar or a state machine โ†’ Mutual recursion.
1 ยท LINEAR 2 ยท TREE (2 calls) 3 ยท MULTI-BRANCH 4 ยท DIVIDE & CONQUER 5 ยท TAIL 6 ยท MUTUAL O(n) chain fib, traversals subsets/perms mergesort split f โ†’ f, O(1)* stack fโ†’gโ†’fโ†’g
Recognize the SHAPE โ†’ know the cost. One child = a chain (linear/tail). Two children = a binary tree. A loop of calls = a fan (subsets/permutations). A balanced split = divide & conquer. The silhouette tells you the recurrence at a glance.
// Shape 4 (multi-branch) is the bridge to backtracking: loop over choices, recurse each.
function countDownChoices(n: number, choices: number[]): number {
  if (n === 0) return 1;                 // one full way to reach 0
  let ways = 0;
  for (const c of choices) {             // ONE recursive call per choice
    if (n - c >= 0) ways += countDownChoices(n - c, choices);
  }
  return ways;                           // bubble up the total count
}
โš ๏ธ Tail-recursion reality check (interview gotcha): in theory a tail call needs O(1) stack, but JavaScript/TypeScript engines (V8, Node) do NOT optimize tail calls โ€” only Safari's JSC does. So in practice every recursive call still costs a stack frame, and deep recursion (depth โ‰ณ 10โดโ€“10โต) overflows regardless of tail position. If depth can be large, convert to a loop or an explicit stack (ยง15). Treat "tail recursion is free" as true for the math, false for the runtime you'll code in.

10. PATTERN: LINEAR RECURSION

One call, then combine with my one element.

One smaller call, combine with the current element. The workhorse. If you can do these four cold, you understand the core loop of recursion.
// factorial โ€” base 1 (multiplicative identity), combine with *
function factorial(n: number): number {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// power a^n in O(log n) via "fast exponentiation" โ€” D&C flavored linear
function power(a: number, n: number): number {
  if (n === 0) return 1;
  const half = power(a, Math.floor(n / 2));   // compute ONCE, reuse โ€” don't call twice!
  return n % 2 === 0 ? half * half : half * half * a;
}

// reverse a string โ€” base "", combine by moving head to the back
function reverse(s: string): string {
  if (s.length <= 1) return s;
  return reverse(s.slice(1)) + s[0];
}

// reverse a linked list โ€” return type is the new HEAD node
class ListNode { val = 0; next: ListNode | null = null; }
function reverseList(head: ListNode | null): ListNode | null {
  if (head === null || head.next === null) return head;  // base: 0 or 1 node
  const newHead = reverseList(head.next);  // trust: rest is reversed, newHead is its tail-end
  head.next.next = head;                    // make the next node point back to me
  head.next = null;                         // I become the new tail
  return newHead;                           // the head never changes as it bubbles up
}
1) Recurse to the END first. newHead = 3 โ€” and it never changes as we unwind. 1 2 3 โ†’ null newHead 2) Unwind at node 2: head.next.next = head (3โ†’2), then head.next = null. 1 2 3 3 now points back to 2 3) Unwind at node 1: 2โ†’1, 1.next = null. Done โ€” return the unchanged newHead. 3 2 1 โ†’ null
Reverse a linked list โ€” why head.next.next = head works. The recursion runs to the tail (which becomes the new head and rides up unchanged); then each frame, on the way UP, flips the single pointer between itself and its next node. You only ever reason about ONE pair โ€” trust the rest is already reversed.
๐ŸŽค Interview lens Probed: state the contract first, then justify why head.next.next = head reverses without losing nodes (you only rewire one pair and trust the rest). Be ready to offer the O(1)-space iterative version. ยท Escalation: reverse the whole list โ†’ reverse the first k โ†’ reverse in k-groups (LC 25) โ†’ swap pairs (LC 24). On n โ‰ณ 10โด, flag the stack-depth risk and give the loop.

11. PATTERN: TREE / BINARY RECURSION

Two calls, combine the two results โ€” almost always post-order.

Two calls, combine the two results. Trees are where recursion feels natural โ€” the structure is literally recursive (a tree is a node plus two smaller trees). Almost always post-order / bubble up.
// Each: contract โ†’ base โ†’ combine. Same skeleton, different combine.
function maxDepth(node: TreeNode | null): number {           // LC 104
  if (node === null) return 0;
  return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));
}

function sumTree(node: TreeNode | null): number {            // sum of all values
  if (node === null) return 0;
  return node.val + sumTree(node.left) + sumTree(node.right);
}

function invert(node: TreeNode | null): TreeNode | null {    // LC 226 โ€” return type is a node
  if (node === null) return null;
  const L = invert(node.left);
  node.left = invert(node.right);
  node.right = L;
  return node;
}

function sameTree(p: TreeNode | null, q: TreeNode | null): boolean {  // LC 100 โ€” boolean combine with &&
  if (p === null && q === null) return true;
  if (p === null || q === null) return false;
  return p.val === q.val && sameTree(p.left, q.left) && sameTree(p.right, q.right);
}
height(node) = 1 + max(height(left), height(right)) โ€” computed on the way UP root a b c d e โ†‘ leaf โ†’ 1 โ†‘ leaf โ†’ 1 โ†‘ leaf โ†’ 1 โ†‘ 1+max(1,1)=2 โ†‘ 1+max(0,1)=2 โ†‘ 1+max(2,2) = 3 Leaves return 1 first; each parent waits for its children, then combines. No value exists until the calls below return.
Return values flow UP and combine. A node can't answer until its children have โ€” so the real computation happens as the stack unwinds. This is the mental picture behind every "compute X of a tree" problem.
๐Ÿ”‘ The "return one thing but track another" trap (e.g. tree diameter, LC 543). When the value you must return to your parent differs from the value you must report globally, return the first and update a closure variable for the second. This pattern appears constantly in tree interviews.
function diameterOfBinaryTree(root: TreeNode | null): number {  // LC 543
  let best = 0;
  function depth(node: TreeNode | null): number {  // RETURNS depth; SIDE-EFFECT updates `best`
    if (node === null) return 0;
    const L = depth(node.left);
    const R = depth(node.right);
    best = Math.max(best, L + R);   // path THROUGH this node (the thing we report)
    return 1 + Math.max(L, R);      // depth (the thing the parent needs)
  }
  depth(root);
  return best;
}
๐ŸŽค Interview lens Probed: choose post-order and say why (your answer needs the children first), and explain the return-vs-report split โ€” return one thing to the parent, update a closure for the global answer. ยท Escalation: maxDepth โ†’ diameter (LC 543) โ†’ "is balanced?" (return -1 as a sentinel to prune early, LC 110) โ†’ max path sum (clamp a negative child to 0, LC 124).

12. PATTERN: DIVIDE & CONQUER

Split in half, trust each half, merge.

Split in half, solve each half (trust it), merge. The recursion does the splitting; your real work is the merge step. Cost follows T(n) = 2ยทT(n/2) + (merge cost).
// merge sort โ€” split, sort halves (trust), merge two sorted lists.  O(n log n)
function mergeSort(a: number[]): number[] {
  if (a.length <= 1) return a;                       // base: 0/1 element is already sorted
  const mid = a.length >> 1;
  const L = mergeSort(a.slice(0, mid));              // trust: returns sorted left
  const R = mergeSort(a.slice(mid));                 // trust: returns sorted right
  return merge(L, R);                                // the ONLY real work
}
function merge(L: number[], R: number[]): number[] {
  const out: number[] = []; let i = 0, j = 0;
  while (i < L.length && j < R.length) out.push(L[i] <= R[j] ? L[i++] : R[j++]);
  while (i < L.length) out.push(L[i++]);
  while (j < R.length) out.push(R[j++]);
  return out;
}

// binary search โ€” throw away half each call.  T(n)=T(n/2)+O(1) โ†’ O(log n)
function bsearch(a: number[], target: number, lo = 0, hi = a.length - 1): number {
  if (lo > hi) return -1;                            // base: empty range โ†’ not found
  const mid = (lo + hi) >> 1;
  if (a[mid] === target) return mid;
  return a[mid] < target ? bsearch(a, target, mid + 1, hi)
                         : bsearch(a, target, lo, mid - 1);
}
โ—ค DIVIDE (down) MERGE (up) โ—ฅ [38, 27, 43, 3] [38, 27] [43, 3] [38] [27] [43] [3] size 1 = sorted โ†‘ [27, 38] โ†‘ [3, 43] [3, 27, 38, 43]
Divide & conquer: split until size 1 (trivially sorted), then MERGE sorted halves on the way up. The merge is the only real work โ†’ T(n) = 2ยทT(n/2) + O(n) = O(n log n).
๐ŸŽค Interview lens Probed: write T(n)=2T(n/2)+O(n) and read off O(n log n); name the merge as the only real work and where the recursion "does nothing." ยท Escalation: merge sort โ†’ count inversions (piggyback the count onto merge) โ†’ sort a linked list in O(1) extra (LC 148) โ†’ quickselect for the k-th largest (average O(n)).

13. PATTERN: PICK / SKIP (subsequences)

Each element: take it or leave it โ†’ 2โฟ leaves.

"For each element, two choices: take it or leave it." This single idea generates subsets, subsequences, knapsack, and combination sums โ€” and it's the cleanest on-ramp from plain recursion into backtracking and DP.
// All subsets via PICK/SKIP โ€” Style B (carry down: build `path`, collect at base).
function subsets(nums: number[]): number[][] {
  const res: number[][] = [];
  function go(i: number, path: number[]): void {
    if (i === nums.length) { res.push([...path]); return; }  // base: decided every element โ†’ record (COPY!)
    // CHOICE 1 โ€” skip nums[i]
    go(i + 1, path);
    // CHOICE 2 โ€” pick nums[i]
    path.push(nums[i]);
    go(i + 1, path);
    path.pop();                 // UNDO so the skip-branch above isn't polluted (backtracking)
  }
  go(0, []);
  return res;
}

// Same idea as a COUNT via Style A (bubble up) โ€” "how many subsets sum to target?"
function countSubsetsSum(nums: number[], i: number, target: number): number {
  if (i === nums.length) return target === 0 ? 1 : 0;          // decided every element โ†’ did we land on target?
  const skip = countSubsetsSum(nums, i + 1, target);           // don't take nums[i]
  const pick = countSubsetsSum(nums, i + 1, target - nums[i]); // take nums[i]
  return skip + pick;                          // combine the two universes
}
Subsets of [1, 2] โ€” each level decides ONE element: skip (left) or pick (right) skip 1 pick 1 skip 2 pick 2 skip 2 pick 2 { } i=0 { } {1} { } {2} {1} {1,2} 4 leaves = 2ยฒ subsets โ€” collect a COPY at each leaf add an UNDO after each "pick" branch โ†’ this becomes backtracking
Pick/skip tree. At each index you branch into skip and pick; the 2โฟ leaves are exactly the subsets. This same tree, with constraints + an undo step, is the engine of all backtracking.
See the bridge: add an undo after the pick branch โ†’ backtracking (full enumeration of valid paths). Add a memo keyed on (i, target) to the count version โ†’ DP. Same recursion, three destinations. Deep-dive in the Backtracking and DP sheets.
๐ŸŽค Interview lens Probed: the pick/skip recurrence, why there are 2โฟ leaves, and copy-on-collect vs. storing a live reference. ยท Escalation: subsets โ†’ subsets with duplicates (sort, then skip equal siblings) โ†’ combination sum โ†’ "count subsets that sum to T," then memoize on (i, target) and you've derived 0/1-knapsack DP live.

14. PASSING STATE DOWN โ€” params vs return vs closure

THREE WAYS TO MOVE INFORMATION THROUGH A RECURSION
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1) RETURN VALUE   info flows UP    "what did my children find?"
                  โ†’ use for: results you combine (Style A)

2) PARAMETER      info flows DOWN  "what do my ancestors know?"
                  โ†’ use for: index, depth, path-so-far, running
                    sum, remaining capacity (accumulator pattern)

3) CLOSURE / outer info flows BOTH "global best / result list"
   variable        โ†’ use for: a single answer many frames update
                     (max so far, results[]), or to avoid threading
                     the same array through every call.

PICK BY ASKING: who needs this info?
   parent only        โ†’ RETURN it
   descendants only   โ†’ pass as PARAMETER
   everybody / one
   shared answer      โ†’ CLOSURE variable
f(n) f(n-1) f(n-2) PARAMETER (down) index ยท path ยท acc ยท depth RETURN (up) the combined answer CLOSURE var: best, res[] any frame reads/writes
Three channels, three directions. Need it from your ancestors? Pass it down as a parameter. Need it from your children? Return it up. One answer shared by everyone (a running max, a results list)? Put it in a closure. Pick by asking "who needs this info?"
// Root-to-leaf path sums (LC 257-style) โ€” depth & path ride DOWN as params,
// results collected via a CLOSURE array, base case records the finished path.
function binaryTreePaths(root: TreeNode | null): string[] {
  const res: string[] = [];
  function dfs(node: TreeNode | null, path: number[]): void {
    if (node === null) return;
    path.push(node.val);                                   // choose
    if (node.left === null && node.right === null) {        // leaf โ†’ finished path
      res.push(path.join("->"));
    } else {
      dfs(node.left, path);
      dfs(node.right, path);
    }
    path.pop();                                            // UNDO (so siblings start clean)
  }
  dfs(root, []);
  return res;
}
โš ๏ธ Copy vs reference โ€” the silent killer. When you collect a mutable array into a results list, push a copy ([...path]). Pushing the array itself stores a live reference that later pop()s will mutate, and every entry ends up empty/identical.

15. RECURSION โ†” ITERATION

Every recursion is a loop + a stack. The runtime's call stack is a stack. Convert when input is huge (avoid stack overflow) or when an interviewer asks for the iterative version.
// Recursive DFS โ€ฆ
function dfsRec(node: TreeNode | null, out: number[]): void {
  if (node === null) return;
  out.push(node.val);            // preorder
  dfsRec(node.left, out);
  dfsRec(node.right, out);
}

// โ€ฆ same traversal, explicit stack (push RIGHT first so LEFT pops first)
function dfsIter(root: TreeNode | null): number[] {
  const out: number[] = [];
  if (root === null) return out;
  const stack: TreeNode[] = [root];
  while (stack.length) {
    const node = stack.pop()!;
    out.push(node.val);
    if (node.right) stack.push(node.right);  // mirror the call order
    if (node.left)  stack.push(node.left);
  }
  return out;
}
CONVERSION RECIPE
  โ€ข tail recursion        โ†’ plain while-loop, update the params
  โ€ข linear recursion      โ†’ loop, often with an accumulator
  โ€ข tree / branching DFS  โ†’ explicit stack of "frames" (state)
  โ€ข BFS / level-order     โ†’ use a QUEUE instead of a stack
WHEN TO BOTHER
  โ€ข depth can exceed ~10^4โ€“10^5 โ†’ risk of stack overflow โ†’ go iterative
  โ€ข otherwise recursion is usually clearer; keep it.

16. MEMOIZATION (the bridge to DP)

Same recursion + a cache = top-down DP.

If your recursion recomputes the same input, cache it. Plain recursion + a cache = top-down DP. You don't redesign anything โ€” you wrap the same function.
fib(5) WITHOUT memo โ€” the same subtrees recomputed (exponential)
                fib(5)
            /            \
        fib(4)          fib(3)
        /    \          /    \
    fib(3)  fib(2)   fib(2)  fib(1)     โ† fib(3), fib(2) computed MANY times
    ...
Recursion tree has ~2^n nodes โ†’ O(2^n).  Memo collapses it to O(n):
each distinct input computed ONCE, then read from cache.
fib(5) without memo โ€” the same calls recomputed (exponential) fib(5) fib(4) fib(3) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) amber = already computed elsewhere = wasted work. fib(3)ร—2, fib(2)ร—3 โ€ฆ cache each distinct call once โ†’ O(n)
Overlapping subproblems. The tree has ~2โฟ nodes, but only n distinct inputs. A memo collapses every amber repeat into a single cached lookup โ€” turning O(2โฟ) into O(n). That collapse is top-down DP.
// O(2^n) โ€” beautiful, but recomputes everything
function fibSlow(n: number): number {
  if (n < 2) return n;
  return fibSlow(n - 1) + fibSlow(n - 2);
}

// O(n) โ€” identical logic, one cache line added. THIS is top-down DP.
function fib(n: number, memo = new Map<number, number>()): number {
  if (n < 2) return n;
  if (memo.has(n)) return memo.get(n)!;          // 1) check cache
  const ans = fib(n - 1, memo) + fib(n - 2, memo);
  memo.set(n, ans);                              // 2) store before returning
  return ans;
}
Memoize when: the answer depends only on the parameters (no hidden mutable state), AND the same parameter tuple recurs. Key the cache on exactly the parameters that change. Full state-design, bottom-up tables, and space optimization live in the DP sheet.
๐ŸŽค Interview lens Probed: spot the overlapping subproblems, key the cache on exactly the changing parameters, and state the time drop (O(2โฟ) โ†’ O(n)). ยท Escalation: memoized fib โ†’ climbing stairs / coin change โ†’ "now do it bottom-up, then in O(1) space." Memo first, table second is the senior move.

17. COMPLEXITY OF RECURSION

Time = (number of nodes in the recursion tree) ร— (work per node). Space = max depth of the tree (the call stack) + space held by return values. Don't guess โ€” write the recurrence, or count tree nodes.
Why T(n) = 2ยทT(n/2) + O(n) is O(n log n) n n/2 n/2 n/4 n/4 n/4 n/4 โ‹ฎ = n = n = n each level sums to n logโ‚‚ n levels (work per level) ร— (number of levels) = n ร— log n โ†’ O(n log n) Sum the tree by ROWS, not nodes: each row does O(n) work, and there are log n rows.
Sum the recursion tree by rows. The subproblems halve each level (โ†’ log n levels), but the O(n) merge work at every level still totals n per row. n per row ร— log n rows = O(n log n). Change the per-node work and the answer changes โ€” that's the whole table above.
RECURRENCE โ†’ COMPLEXITY (memorize this table)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ RECURRENCE              โ”‚ TIME          โ”‚ EXAMPLE               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ T(n)=T(n-1)+O(1)        โ”‚ O(n)          โ”‚ sum, factorial        โ”‚
โ”‚ T(n)=T(n-1)+O(n)        โ”‚ O(n^2)        โ”‚ insertion-ish, perms  โ”‚
โ”‚ T(n)=2T(n-1)+O(1)       โ”‚ O(2^n)        โ”‚ naive fib, subsets    โ”‚
โ”‚ T(n)=T(n/2)+O(1)        โ”‚ O(log n)      โ”‚ binary search         โ”‚
โ”‚ T(n)=2T(n/2)+O(1)       โ”‚ O(n)          โ”‚ tree size/height      โ”‚
โ”‚ T(n)=2T(n/2)+O(n)       โ”‚ O(n log n)    โ”‚ merge / quick sort    โ”‚
โ”‚ T(n)=kยทT(n-1)+O(1)      โ”‚ O(k^n)        โ”‚ k-ary choice trees    โ”‚
โ”‚ branch^depth nodes      โ”‚ O(b^d)        โ”‚ general backtracking  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

SPACE โ€” people forget the stack!
  depth of recursion = frames alive at once = stack space.
  โ€ข linear recursion over n  โ†’ O(n) stack  (NOT O(1)!)
  โ€ข balanced tree            โ†’ O(log n) stack (height)
  โ€ข skewed tree / list       โ†’ O(n) stack
  Add space for the output (e.g. subsets store O(nยท2^n) total).

MASTER THEOREM (quick form) for T(n)=aยทT(n/b)+O(n^d):
  a < b^d โ†’ O(n^d)        (work dominated by the top)
  a = b^d โ†’ O(n^d log n)  (balanced โ€” e.g. mergesort: a=2,b=2,d=1)
  a > b^d โ†’ O(n^(log_b a)) (work dominated by the leaves)

18. COMMON BUGS & FIXES

SYMPTOM                         CAUSE โ†’ FIX
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
"Maximum call stack exceeded"   No/unreachable base case, or no
                                progress. โ†’ ensure input strictly
                                shrinks; use >= / <= not == on indices.

Returns undefined / NaN         Forgot `return` on the recursive line.
                                `f(n-1)` โ‰  `return f(n-1)`. โ†’ add return.

All results are empty/identical  Stored a REFERENCE to a mutated array.
                                โ†’ push a COPY: res.push([...path]).

Wrong answer, off by one         Base case returns wrong identity, or
                                base & recursive return DIFFERENT types.
                                โ†’ re-check Step 2 of section 6.

Backtracking gives extra junk    Forgot to UNDO (pop / unmark) after
                                the recursive call. โ†’ mirror every
                                "choose" with an "unchoose".

TLE on fib-like problems         Recomputing subproblems. โ†’ add a memo
                                (section 16) โ†’ top-down DP.

Recurses into null and crashes   Leaf-style base case but a node has
                                ONE child. โ†’ guard on `node === null`.

Computes child twice (slow)      Called f(...) twice instead of saving
                                it. โ†’ const half = f(...); use `half`.
๐Ÿ”‘ The 30-second self-check before you run it: (1) Is there a base case for every smallest input? (2) Does every recursive call get a strictly smaller argument? (3) Does every code path return the right type? (4) If I mutate shared state, do I undo it and copy on collect? Four yeses โ†’ it almost certainly works.

19. KEYWORD โ†’ APPROACH MAP

Phrase in the problemRecursion shape / style
"compute X of the whole structure"bubble up (return + combine)
"height / depth / count / sum of tree"post-order, Style A
"does a path/element exist?"boolean return with || / &&
"all ... / every ... / list them"carry down + collect (Style B)
"all paths root to leaf"params for path + closure result
"generate all subsets/combos/perms"pick/skip or for-each-choice + undo
"split / halve / sorted"divide & conquer
"nth term / ways to ..."linear/branching; likely + memo
"same subproblem repeats"add memo โ†’ DP
"reverse / build a list or tree"return the node; reattach on the way up
"process in order 1..n" / "n..1"work before vs after the call (ยง8)
"too deep / n up to 1e6"convert to iteration + explicit stack

โž• Parsing with recursion, level 2 โ€” Basic Calculator III (LC 772)

224 (parentheses) + 227 (precedence) in one function: recurse on (, fold * and / immediately.

๐Ÿ”‘ Invariant Each call evaluates exactly one parenthesis level and returns its value; the caller treats that value as a plain number operand. The per-level stack trick from 227 handles precedence: push numbers, fold *// with the top, sum at the end.
// 772 Basic Calculator III โ€” recursion replaces the explicit stack for '(' โ€ฆ ')'
function calculate(s: string): number {
  let i = 0;
  const isDigit = (c: string) => c >= '0' && c <= '9';
  const parse = (): number => {
    const stack: number[] = [];
    let num = 0, op = '+';
    while (i < s.length) {
      const c = s[i++];
      if (isDigit(c)) num = num * 10 + (+c);
      else if (c === '(') num = parse();                      // the whole (...) becomes ONE operand
      if ((!isDigit(c) && c !== ' ' && c !== '(') || i === s.length) {
        if (op === '+') stack.push(num);
        else if (op === '-') stack.push(-num);
        else if (op === '*') stack.push(stack.pop()! * num);   // * and / fold immediately (precedence)
        else if (op === '/') stack.push(Math.trunc(stack.pop()! / num));
        op = c; num = 0;
      }
      if (c === ')') break;                                    // finished this parenthesis level
    }
    return stack.reduce((a, b) => a + b, 0);
  };
  return parse();
}
โš ๏ธ Trap Consume the ) before returning; division truncates toward zero (Math.trunc); apply the pending operator when the next operator, ) or the end arrives โ€” not when you read the number.

โž• Build by wrapping โ€” Strobogrammatic Number II (LC 247)

Answer for length n = every answer for n โˆ’ 2 wrapped by a rotation-valid pair.

๐Ÿ”‘ Invariant gen(m) returns all valid numbers of length m: base cases [""] and ["0","1","8"]; otherwise wrap each gen(mโˆ’2) result with 1โ€ฆ1, 6โ€ฆ9, 8โ€ฆ8, 9โ€ฆ6 (and 0โ€ฆ0 only when this is not the outermost layer).
// 247 Strobogrammatic Number II โ€” build from the middle outward
function findStrobogrammatic(n: number): string[] {
  const gen = (m: number): string[] => {
    if (m === 0) return [''];
    if (m === 1) return ['0', '1', '8'];
    const pairs = [['1', '1'], ['6', '9'], ['8', '8'], ['9', '6']];
    if (m !== n) pairs.push(['0', '0']);                      // 0โ€ฆ0 only for INNER layers (no leading zero)
    const res: string[] = [];
    for (const mid of gen(m - 2)) for (const [a, b] of pairs) res.push(a + mid + b);
    return res;
  };
  return gen(n);
}
โš ๏ธ Trap A leading zero is illegal, so 0โ€ฆ0 is only allowed on inner layers (m !== n). 6 pairs with 9, and 9 with 6.

โ–ถ Coverage โ€” what these patterns unlock on LeetCode

If you can write each pattern cold, the problems below are in range. Bold = Hard tier โ€” the escalation an interviewer reaches for once you've nailed the medium.

Pattern (ยง)Representative LeetCode (medium โ†’ hard)Tier
Linear (ยง10)206 Reverse List ยท 24 Swap Pairs ยท 50 Pow(x,n) ยท 25 Reverse Nodes in k-GroupEasy โ†’ Hard
Tree / binary (ยง11)104 Max Depth ยท 226 Invert ยท 543 Diameter ยท 110 Balanced ยท 124 Max Path SumEasy โ†’ Hard
Divide & conquer (ยง12)148 Sort List ยท 215 Kth Largest ยท 23 Merge k Lists ยท 315 Count of SmallerMed โ†’ Hard
Pick / skip (ยง13)78 Subsets ยท 90 Subsets II ยท 39 Combination Sum ยท 416 Partition Equal SubsetMed
State down + collect (ยง14)112 Path Sum ยท 257 Binary Tree Paths ยท 113 Path Sum II ยท 129 Sum Rootโ†’LeafEasy โ†’ Med
Recursion โ†’ iteration (ยง15)94 Inorder ยท 230 Kth Smallest in BST ยท 114 Flatten ยท 173 BST IteratorMed
Memo โ†’ DP (ยง16)70 Climb Stairs ยท 198 House Robber ยท 322 Coin Change ยท 91 Decode Ways ยท 312 Burst BalloonsEasy โ†’ Hard
Number / grammar D&C779 K-th Symbol ยท 241 Different Ways to Add Parentheses ยท 395 Longest Substring with โ‰ฅ k Repeats ยท 247 Strobogrammatic II ยท 772 Basic Calculator IIIMed

20. RECURSION IS THE ENGINE OF DSA

Recursion is not a topic you finish โ€” it's the substrate under most of the interview syllabus. Once the Leap of Faith is automatic, these "separate" topics stop being separate: each is recursion with one extra idea bolted on. This is why mastering recursion pays off everywhere.
RECURSION contract + trust Trees node + 2 subtrees Dynamic Programming recursion + memo Backtracking recursion + undo Divide & Conquer split ยท recurse ยท merge Graph DFS recurse neighbors + seen Combinatorics pick / skip each item
Six interview topics, one engine. Each is recursion plus a single twist: a cache (DP), an undo (backtracking), a split/merge (D&C), a visited-set (graph DFS), or a take/leave choice (combinatorics). Get recursion deep and you've pre-loaded all six.
DSA TOPIC          = RECURSION THAT โ€ฆ                          HERE
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
Tree problems      โ€ฆ recurses on node.left and node.right       ยง11
Divide & conquer   โ€ฆ splits in half, solves each, merges        ยง12
Backtracking       โ€ฆ loops over choices + UNDOes after each     ยง13 โ†’ backtracking.html
Top-down DP        โ€ฆ caches answers so repeats are free         ยง16 โ†’ dp.html
Graph DFS          โ€ฆ recurses on neighbors with a visited set   ยง9  โ†’ graph.html
Subsets/perms      โ€ฆ makes a pick/skip choice per element       ยง13
Sorting (merge/qk) โ€ฆ divide & conquer on the array              ยง12
Binary search      โ€ฆ recurses on the surviving half             ยง12

THE PUNCHLINE: if you can answer "what does f return, and how do I
build it from f(smaller)?" โ€” you can do ALL of the above. They differ
only in WHAT you carry down (a choice, a cache key, a visited set) and
WHAT you do on the way up (combine, count, collect, undo).

21. SELF-TEST โ€” ACTIVE RECALL

Reading recursion โ‰  being able to write it. Cover the right column. If you can produce these from memory, you're interview-ready. If any one stalls you, that's your signal for which section to re-read. Redo this weekly until it's instant.

Cover the right column (it's blurred โ€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
The one mental move recursion needs?Assume f(smaller) already works; do exactly one level.
The 4 questions, in order?contract ยท base ยท leap ยท progress
Base case = the ___ of the combine op?identity (+โ†’0, *โ†’1, ||โ†’false, concatโ†’"")
What makes a recursion run forever?No base case / no progress โ€” test <=, not ==, on indices.
Return line for "sum of a[i..]"?a[i] + sum(a, i+1)
Return line for tree height?1 + max(depth(L), depth(R))
Return line for "tree contains x"?node.val===x || f(L) || f(R)
"All paths / all subsets" โ†’ which style?Carry-down params + collect a copy at the base.
"Compute one value of the whole tree" โ†’ style?Bubble up: return combine(L, R).
Where does post-order work run?AFTER the calls (on the way UP).
Make fib O(n) instead of O(2โฟ)?Memoize on n.
Convert deep recursion to avoid stack overflow?Explicit stack (DFS) / queue (BFS).
Time of T(n)=2T(n/2)+O(n)?O(n log n) โ€” n work ร— log n levels.
Space cost of recursion?Max depth of the call stack (+ the output).
Most common bug when collecting results?Pushed a live reference, not a [...path] copy.
Basic Calculator III โ€” how does recursion replace the stack for parentheses?On ( call parse() to evaluate that level and use its return as one operand; on ) finish the level. Precedence is handled inside a level by folding *// immediately.
Strobogrammatic Number II โ€” why is 0โ€ฆ0 only for inner layers?The outermost pair would put a 0 at the front (a leading zero), which is not a valid number; inside, zeros are fine.
Then write these cold (no peeking, in TypeScript): (1) reverse a string, (2) max depth of a binary tree, (3) all subsets of [1,2,3], (4) climb-stairs count with a memo, (5) merge sort. If you can produce all five start-to-finish, you can recurse at interview level โ€” the rest is just recognizing which shape (ยง9) the new problem is.