What happens if there is no base case?
Infinite recursion until stack overflow—program crashes.
Master C++ recursion: understand recursion vs iteration, recursive algorithms, stack management, optimization techniques, and when to use recursion effectively.
Base Case & Recursive Case
Comparison & Trade-offs
Stack Frames & Memory
Tail Recursion & Memoization
Recursion solves problems by reducing them to smaller subproblems. Learn when recursion fits, how the call stack works, and how to avoid stack overflow and redundant work.
Tree/graph DFS, divide-and-conquer, and backtracking rely on recursion. Interviewers love factorial, Fibonacci, and tower-of-Hanoi variants.
Recursion is a programming technique where a function calls itself to solve a problem. It breaks down complex problems into simpler, similar subproblems until a base case is reached.
The following table provides a detailed comparison between iteration and recursion in C++:
| Aspect | Iteration (Loops) | Recursion (Self-calling) |
|---|---|---|
| Definition | Repeats block of code using loops (for, while, do-while) | Function calls itself to solve smaller instances of same problem |
| Basic Structure |
for(int i=0; i while(condition) { } |
type func(params) { if(baseCase) return value; return func(modifiedParams); } |
| Memory Usage |
Low memory Uses constant stack space |
High memory Each call creates stack frame Risk of stack overflow |
| Performance |
Generally faster No function call overhead Better cache locality |
Slower Function call overhead Stack manipulation costs |
| Code Readability |
Simple for linear problems Easy to debug Straightforward flow |
Elegant for certain problems Matches problem definition Can be harder to understand |
| When to Use |
• Simple repetition • Sequential processing • Performance-critical code • Known number of iterations |
• Tree/graph traversal • Divide and conquer • Backtracking problems • Mathematical definitions |
| Termination |
Loop condition becomes false Explicit break statement |
Base case reached Must be carefully defined |
| Debugging |
Easier to debug Linear execution flow Can use loop variables |
Harder to debug Multiple stack frames Complex call hierarchy |
| Examples |
• Array processing • Counting/summing • Searching arrays • Matrix operations |
• Factorial calculation • Fibonacci sequence • Tree traversal • Tower of Hanoi |
| Risk Factors |
• Infinite loops • Off-by-one errors • Incorrect loop conditions |
• Stack overflow • Infinite recursion • Missing base case • Exponential time complexity |
Any recursive algorithm can be converted to iteration (using stacks), and most iterative algorithms can be converted to recursion. The choice depends on problem nature, readability, and performance requirements.
These classic examples demonstrate the fundamental pattern of recursion: breaking down problems into smaller subproblems.
return_type recursiveFunction(parameters) {
// 1. BASE CASE (stopping condition)
if (simple_case_condition) {
return base_value;
}
// 2. RECURSIVE CASE (break down problem)
// Modify parameters to make problem smaller
return combine(recursiveFunction(smaller_problem), current_value);
}
long long fact(int n) {
long long r = 1;
for (int i = 2; i <= n; i++)
r *= i;
return r;
}long long fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}if (n == 0)
return 1; // 0! = 1cout << fact(5);
// 5*4*3*2*1 = 120if (n < 0)
throw invalid_argument("n");
return fact(n);cout << factLoop(6);
cout << factRec(6);
// both print 720int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}int a=0, b=1;
for (int i=2; i<=n; i++) {
int t = a+b; a=b; b=t;
}
return b;cout << fib(0); // 0
cout << fib(1); // 1if (memo[n] != -1)
return memo[n];
memo[n] = fib(n-1)+fib(n-2);
return memo[n];dp[0]=0; dp[1]=1;
for (int i=2; i<=n; i++)
dp[i]=dp[i-1]+dp[i-2];
return dp[n];// 0,1,1,2,3,5
for (int i=0; i<6; i++)
cout << fib(i) << " ";Understanding the call stack is crucial for debugging recursive functions. Each recursive call creates a new stack frame with its own parameters and local variables.
long long fact(int n, int d=0) {
// d tracks stack depth
return n <= 1 ? 1 : n*fact(n-1,d+1);
}void indent(int d) {
for (int i=0; i<d; i++)
cout << "| ";
}indent(d);
cout << "-> fact(" << n << ")\n";if (n <= 1) {
indent(d);
cout << "<- return 1\n";
return 1;
}// frames: fact(3), fact(2),
// fact(1) then unwind// fib(2) computed twice
// in naive fib(4) treeChoosing between recursion and iteration depends on problem characteristics, performance requirements, and code clarity.
| Problem Type | Use Iteration When... | Use Recursion When... |
|---|---|---|
| Tree/Graph Traversal |
Not ideal Requires explicit stack/queue More complex implementation |
Excellent fit DFS naturally recursive Clean, readable code Examples: Binary tree traversal |
| Mathematical Calculations |
Usually better Factorial, Fibonacci (iterative) Better performance No stack overflow risk |
Sometimes useful Matches mathematical definition Readable for simple cases Risk for large inputs |
| Divide and Conquer |
Challenging Merge sort (iterative is complex) Quick sort (needs explicit stack) |
Natural fit Merge sort (clean recursive) Quick sort (elegant recursive) Binary search (recursive) |
| Backtracking Problems |
Very complex N-Queens (hard with iteration) Sudoku solver (messy iterative) |
Perfect match N-Queens (clean recursive) Sudoku solver (elegant) Maze solving (natural) |
| Dynamic Programming |
Usually preferred Bottom-up DP (iterative) Better space optimization No recursion overhead |
Top-down approach Memoization (recursive + cache) Easier to implement initially Can be optimized later |
| File System Navigation |
Complex Need explicit stack Harder to read/maintain |
Ideal solution Directory traversal Clean, readable code Matches hierarchical structure |
| Performance Critical |
Always better No function call overhead Better cache performance Predictable performance |
Avoid if possible Function call overhead Stack memory usage Possible cache misses |
| Code Readability |
Simpler for linear problems Easier for beginners Direct control flow |
Elegant for certain problems Expresses problem nature Can be more declarative |
void inorder(TreeNode* n) {
if (!n) return;
inorder(n->left);
cout << n->val << " ";
inorder(n->right);
}stack<TreeNode*> st;
while (cur || !st.empty()) {
while (cur) { st.push(cur); cur=cur->left; }
cur = st.top(); st.pop();
cout << cur->val; cur=cur->right;
}int bs(vector<int>& a, int lo, int hi, int x) {
if (lo > hi) return -1;
int m = lo + (hi-lo)/2;
if (a[m]==x) return m;
return a[m]<x ? bs(a,m+1,hi,x) : bs(a,lo,m-1,x);
}void mergeSort(vector<int>& v, int l, int r) {
if (l >= r) return;
int m = l + (r-l)/2;
mergeSort(v,l,m); mergeSort(v,m+1,r);
}int sum = 0;
for (int x : nums) sum += x;
cout << sum;int sumRec(vector<int>& a, int i) {
if (i >= (int)a.size()) return 0;
return a[i] + sumRec(a, i+1);
}Tail recursion occurs when the recursive call is the last operation in the function. Compilers can optimize tail recursion to avoid stack growth, making it as efficient as iteration.
// REGULAR RECURSION (not tail)
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Multiplication AFTER call
}
// TAIL RECURSION (can be optimized)
int factorialTail(int n, int accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // Call is LAST operation
}
return n * fact(n-1);
// multiply AFTER call returnslong long factT(int n, long long acc=1) {
if (n <= 1) return acc;
return factT(n-1, n*acc);
}int sumT(int n, int acc=0) {
if (n <= 0) return acc;
return sumT(n-1, acc+n);
}int gcd(int a, int b) {
if (b==0) return a;
return gcd(b, a % b);
}int fibT(int n,int a=0,int b=1) {
if (n==0) return a;
return fibT(n-1,b,a+b);
}return n + sum(n-1);
// addition after recursive returnint bad(int n) {
return n * bad(n-1); // never stops
}int stuck(int n) {
if (n==0) return 0;
return stuck(n); // same n
}int fact(int n) {
if (n<=1) return 1;
return n*fact(n-1);
}if (memo.count(n)) return memo[n];
memo[n]=fib(n-1)+fib(n-2);
return memo[n];int sumDig(int n) {
if (n<10) return n;
return n%10 + sumDig(n/10);
}void walk(FileNode* f, int d=0) {
cout << string(d*2,' ') << f->name;
for (auto* c : f->kids) walk(c,d+1);
}Infinite recursion until stack overflow—program crashes.
Often due to call overhead, but clarity wins for tree problems; iteration can be faster for linear work.
Recursive call is the last operation; compilers may optimize to iteration (not guaranteed in C++).