๐ช 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 itn 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.| # | Archetype | Info flow | Signal phrases in the problem | Mechanics / sheet |
|---|---|---|---|---|
| 1 | Bubble-up | UP only (return) | "compute the X of the whole structure" โ sum, height, count, does-a-path-exist | ยง5 Style A |
| 2 | Carry-down | DOWN (parameters) | index / remaining budget / path-so-far belongs in the signature; "process in order" | ยง5 Style B, ยง14 |
| 3 | Global-answer | SIDEWAYS (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 |
| 4 | Explore-all | DOWN + undo | "all subsets/combinations/permutations", "find every way toโฆ", "list them" | multi-branch shape ยง9 โ backtracking.html |
| 5 | Remember (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 singlereturnin 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.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
}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 flow | UP the stack: child returns โ parent combines โ parent returns up. | DOWN the stack: parent hands the partial answer to the child as an argument. |
| Returns | A meaningful value (number, list, bool, node). | Often void โ pushes the finished answer into an external array once it reaches the base case. |
| Use when | You 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. |
| Examples | sum, 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." |
// 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.
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.
| Problem | Base return | Recursive return (combine) |
|---|---|---|
| length of list | 0 | 1 + f(rest) |
| sum of list | 0 | head + f(rest) |
| max of list | a[i] | max(a[i], f(i+1)) |
| n! | 1 | n * f(n-1) |
| a^n | 1 | a * f(n-1) |
| contains x? | false | a[i]===x || f(i+1) |
| all positive? | true | a[i]>0 && f(i+1) |
| reverse string | "" | f(rest) + s[0] |
| tree node count | 0 (null) | 1 + f(left) + f(right) |
| tree height | 0 (null) | 1 + max(f(left), f(right)) |
| tree contains x | false | node.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 isfalse,&&'s istrue, string concat's is"". Some operations (likemax/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 returnsnull/[], 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?=== 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)
}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 resultsHow 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 | # calls | Signature feeling | Typical cost |
|---|---|---|---|
| 1 Linear | 1 call | f(n-1) once โ factorial, sum, list walk | O(n) time, O(n) stack |
| 2 Tail | 1 call | f(...) is the LAST thing; answer fully in the param | O(n) time, O(1)* stack (*if engine optimizes) |
| 3 Binary / Tree | 2 calls | f(left) and f(right) โ fib, tree traversals | often O(2โฟ) or O(n) |
| 4 Multi-branch | k calls | a loop of recursive calls (one per choice) | O(k^depth) โ subsets, permutations, n-ary trees |
| 5 Divide & Conquer | 2 of n/2 | split, solve halves, merge โ mergesort, quicksort, bsearch | T(n) = 2T(n/2) + f(n) |
| 6 Mutual / Nested | via other | f 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.
// 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
}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.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);
}๐ 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;
}-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);
}T(n) = 2ยทT(n/2) + O(n) = O(n log n).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
}2โฟ leaves are exactly the subsets. This same tree, with constraints + an undo step, is the engine of all backtracking.See the bridge: add anundoafter 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.
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// 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 laterpop()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.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.
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.
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 problem | Recursion 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.
*// 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();
}) 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.
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);
}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-Group | Easy โ Hard |
| Tree / binary (ยง11) | 104 Max Depth ยท 226 Invert ยท 543 Diameter ยท 110 Balanced ยท 124 Max Path Sum | Easy โ Hard |
| Divide & conquer (ยง12) | 148 Sort List ยท 215 Kth Largest ยท 23 Merge k Lists ยท 315 Count of Smaller | Med โ Hard |
| Pick / skip (ยง13) | 78 Subsets ยท 90 Subsets II ยท 39 Combination Sum ยท 416 Partition Equal Subset | Med |
| State down + collect (ยง14) | 112 Path Sum ยท 257 Binary Tree Paths ยท 113 Path Sum II ยท 129 Sum RootโLeaf | Easy โ Med |
| Recursion โ iteration (ยง15) | 94 Inorder ยท 230 Kth Smallest in BST ยท 114 Flatten ยท 173 BST Iterator | Med |
| Memo โ DP (ยง16) | 70 Climb Stairs ยท 198 House Robber ยท 322 Coin Change ยท 91 Decode Ways ยท 312 Burst Balloons | Easy โ Hard |
| Number / grammar D&C | 779 K-th Symbol ยท 241 Different Ways to Add Parentheses ยท 395 Longest Substring with โฅ k Repeats ยท 247 Strobogrammatic II ยท 772 Basic Calculator III | Med |
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.
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.
| Prompt | Answer |
|---|---|
| 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.