Recursive functions come in a few shapes, and the shape decides how much time and memory they need. The useful way to see any recursion is its call tree: each call is a node, and the calls it makes are its children. Running the function walks that tree depth-first — all the way down the first branch, back up, then down the next.
isEven(n) calling isOdd(n − 1). Parsers for nested expressions work this way.Every call tree is walked depth-first. What changes is when a call does its own work relative to its recursive calls — exactly like pre-order and post-order traversals of a binary tree:
The same function prints 3, 2, 1 or 1, 2, 3 depending only on where the line sits:
function countDown(n, out = []) { if (n === 0) return out; out.push(n); // work before the call: comes out first return countDown(n - 1, out);}function countUp(n, out = []) { if (n === 0) return out; countUp(n - 1, out); // the call first ... out.push(n); // ... then the work: comes out last return out;}Watch the call tree grow depth-first, and notice the same small calls being solved again and again.
function fib(n) { if (n <= 1) return n; // base cases: fib(0) = 0, fib(1) = 1 return fib(n - 1) + fib(n - 2); // two calls: the call tree branches}shape example calls (time) depth (stack memory)
linear factorial(n) n n
tree, 2 branches fib(n) about 1.6^n n
divide and conquer merge sort about 2n log n
halving, one branch binary search log n log n
backtracking subsets of n 2^n nTwo rules cover almost everything: time ≈ number of calls × work per call, and space ≈ maximum depth × memory per frame (plus anything you store).