Lecture 8
The Recursive Pattern
A recursive function is one that calls itself. Every correct recursive function has two parts:
- Base case — a condition under which the function returns directly without calling itself.
- Recursive case — a call to itself with a strictly simpler input, moving toward the base case.
Without a base case, the function calls itself forever until the program crashes with a stack overflow.
The call stack
Each function call gets its own stack frame — its own copy of local variables and the point to return to. Recursive calls pile up frames until the base case is hit, then unwind:
Each return propagates back up through the chain of pending calls.
Every active call occupies stack memory. Recursing too deeply — either from a missing base case or a very large input — exhausts the stack and crashes the program. The default stack depth is typically a few thousand frames.
Linear Recursion
A function that makes exactly one recursive call per invocation is linearly recursive. Each call reduces the problem by one step.
Factorial
Power
A naive power function makes n recursive calls:
A smarter version halves the problem each time — O(log n):
power(2, 8) now makes calls for exponents 8 → 4 → 2 → 1 → 0 rather than 8 → 7 → ... → 0.
Multiple Recursion
A function that makes more than one recursive call per invocation is multiply recursive. The call tree branches, and the total number of calls grows exponentially.
Fibonacci
The first several values: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
This is correct but inefficient. fib(5) recomputes fib(3) twice, fib(2) three times, and so
on. The number of calls to compute fib(n) grows roughly as 2ⁿ.
Storing already-computed results — called memoization — reduces the Fibonacci recursion to O(n) time. This is the foundation of dynamic programming, which you'll encounter in later courses.
Tail Recursion
A recursive call is tail-recursive if it is the very last operation in the function — nothing happens to its return value before it is returned to the caller.
Compare the two factorial implementations:
The second form carries a running product (acc) forward into each call instead of doing work on
the way back up. Tail-recursive calls can be optimized by the compiler into a loop — no new stack
frame needed — making them as efficient as iteration.
C++ does not guarantee tail-call optimization, but most compilers apply it under optimization
flags (-O2, -O3). In practice, the accumulator pattern is still worth knowing — it directly
expresses the iterative structure of the computation.
Mutual Recursion
Two functions are mutually recursive when each calls the other. Both must have base cases that break the cycle.
Forward declarations are required when two functions reference each other — the compiler must know
is_odd exists before it sees the body of is_even.
Recursion on Sequences
Recursion applies naturally to sequences: peel off one element, recurse on the rest.
Recursive sum
Binary search
Binary search is naturally recursive: compare the middle element, then recurse on whichever half could contain the target. The vector must be sorted.
Each call halves the search range, giving O(log n) time.
mid = lo + (hi - lo) / 2 instead of (lo + hi) / 2 avoids integer overflow when lo and hi
are large.
Exercises
Q1.
Without running the code, predict what this program prints. Trace the call stack step by step.
Q2.
Without running the code, predict what this program prints.
Q3.
Trace through this program step by step. For each call to power, write the arguments and return value.
Q4.
Each snippet has a bug related to recursion. Identify what's wrong and what happens at runtime.
Q5.
Write a recursive function count_if_rec(const std::vector<int>& v, int target, int i) that
returns how many times target appears in v, starting from index i. Then wrap it with a
convenience overload that starts from index 0.
Q6.
Write a recursive function is_palindrome(const std::string& s, int lo, int hi) that returns
true if the characters of s between indices lo and hi (inclusive) form a palindrome.
Wrap it with a convenience function is_palindrome(const std::string& s) that checks the whole
string.
Q7.
Rewrite power(int base, int exp) using the accumulator (tail-recursive) pattern. The function
signature should be power_tail(int base, int exp, int acc = 1). Use simple O(n) recursion
(not fast exponentiation) so the accumulator logic stays clear.
Practice Problems
- Fibonacci Number — Easy
- Merge Two Sorted Lists — Easy
- Pow(x, n) — Medium