C++ Recursion: Complete Guide with Examples
Master C++ recursion: understand recursion vs iteration, recursive algorithms, stack management, optimization techniques, and when to use recursion effectively.
Recursion Basics
Base Case & Recursive Case
vs Iteration
Comparison & Trade-offs
Recursion Stack
Stack Frames & Memory
Optimization
Tail Recursion & Memoization
Introduction to Recursion
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.
Why Use Recursion?
- Elegant solution for certain problems
- Natural fit for tree/graph traversal
- Simplifies divide-and-conquer algorithms
- Easier to understand for some problems
- Useful for backtracking problems
- Natural for mathematical definitions
Recursion Components
- Base Case: Stopping condition
- Recursive Case: Calls itself
- Recursive Call: Function calling itself
- Stack Frames: Memory for each call
- Return Values: Propagate back up
Recursive Function Execution Flow
Iteration vs Recursion: Comprehensive Comparison
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 |
Key Insight: Iteration vs Recursion
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.
1. Basic Recursion Examples: Factorial & Fibonacci
These classic examples demonstrate the fundamental pattern of recursion: breaking down problems into smaller subproblems.
Recursive Pattern Template
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);
}
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
// Function prototypes
long long factorialIterative(int n);
long long factorialRecursive(int n);
void comparePerformance(int n);
int main() {
cout << "=== FACTORIAL: ITERATION vs RECURSION ===" << endl << endl;
// Calculate factorial using both methods
int n = 10;
cout << "Calculating " << n << "! (10 factorial)" << endl;
cout << "Iterative: " << n << "! = " << factorialIterative(n) << endl;
cout << "Recursive: " << n << "! = " << factorialRecursive(n) << endl;
cout << endl << "=======================================" << endl << endl;
// Test multiple values
cout << "Factorials from 0 to 10:" << endl;
cout << "n\tIterative\tRecursive" << endl;
cout << "-\t---------\t---------" << endl;
for (int i = 0; i <= 10; i++) {
cout << i << "\t" << factorialIterative(i)
<< "\t\t" << factorialRecursive(i) << endl;
}
cout << endl << "=======================================" << endl << endl;
// Performance comparison
cout << "Performance Comparison (n=20):" << endl;
comparePerformance(20);
// Error case: Negative number
cout << endl << "Error Handling Test:" << endl;
try {
cout << "Factorial of -5 (iterative): ";
cout << factorialIterative(-5) << endl;
} catch (const invalid_argument& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
// ITERATIVE FACTORIAL
long long factorialIterative(int n) {
if (n < 0) {
throw invalid_argument("Factorial not defined for negative numbers");
}
long long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// RECURSIVE FACTORIAL
long long factorialRecursive(int n) {
// Base case: 0! = 1 and 1! = 1
if (n < 0) {
throw invalid_argument("Factorial not defined for negative numbers");
}
if (n <= 1) {
return 1;
}
// Recursive case: n! = n * (n-1)!
return n * factorialRecursive(n - 1);
}
// Performance comparison function
void comparePerformance(int n) {
// Test iterative version
auto start = high_resolution_clock::now();
long long iterResult = factorialIterative(n);
auto stop = high_resolution_clock::now();
auto iterDuration = duration_cast<microseconds>(stop - start);
// Test recursive version
start = high_resolution_clock::now();
long long recurResult = factorialRecursive(n);
stop = high_resolution_clock::now();
auto recurDuration = duration_cast<microseconds>(stop - start);
cout << "Iterative result: " << iterResult << endl;
cout << "Iterative time: " << iterDuration.count() << " microseconds" << endl;
cout << "Recursive result: " << recurResult << endl;
cout << "Recursive time: " << recurDuration.count() << " microseconds" << endl;
if (iterDuration < recurDuration) {
cout << "Iterative was " << (recurDuration.count() - iterDuration.count())
<< " microseconds faster" << endl;
} else {
cout << "Recursive was " << (iterDuration.count() - recurDuration.count())
<< " microseconds faster" << endl;
}
}
#include <iostream>
#include <vector>
#include <chrono>
using namespace std;
using namespace std::chrono;
// Fibonacci function prototypes
int fibonacciIterative(int n);
int fibonacciRecursive(int n);
int fibonacciMemoization(int n);
int fibonacciDP(int n);
void printFibonacciComparison(int terms);
int main() {
cout << "=== FIBONACCI SEQUENCE: MULTIPLE APPROACHES ===" << endl << endl;
// First 15 Fibonacci numbers
cout << "First 15 Fibonacci numbers:" << endl;
printFibonacciComparison(15);
cout << endl << "==========================================" << endl << endl;
// Performance comparison for larger n
cout << "Performance Comparison (n=30):" << endl;
cout << "Note: Naive recursion becomes extremely slow!" << endl << endl;
int n = 30;
// 1. Iterative (fastest)
auto start = high_resolution_clock::now();
int iterResult = fibonacciIterative(n);
auto stop = high_resolution_clock::now();
auto iterTime = duration_cast<microseconds>(stop - start);
// 2. Memoization (optimized recursion)
start = high_resolution_clock::now();
int memoResult = fibonacciMemoization(n);
stop = high_resolution_clock::now();
auto memoTime = duration_cast<microseconds>(stop - start);
// 3. Dynamic Programming
start = high_resolution_clock::now();
int dpResult = fibonacciDP(n);
stop = high_resolution_clock::now();
auto dpTime = duration_cast<microseconds>(stop - start);
// 4. Naive Recursive (VERY SLOW for n=30)
cout << "Warning: Naive recursive Fibonacci(30) takes significant time..." << endl;
start = high_resolution_clock::now();
int recurResult = fibonacciRecursive(n);
stop = high_resolution_clock::now();
auto recurTime = duration_cast<milliseconds>(stop - start);
cout << endl << "Results for Fibonacci(" << n << "):" << endl;
cout << "All methods should give: " << iterResult << endl << endl;
cout << "Performance Summary:" << endl;
cout << "1. Iterative: " << iterTime.count() << " microseconds" << endl;
cout << "2. Memoization: " << memoTime.count() << " microseconds" << endl;
cout << "3. Dynamic Prog: " << dpTime.count() << " microseconds" << endl;
cout << "4. Naive Recursion: " << recurTime.count() << " milliseconds" << endl;
cout << endl << "==========================================" << endl << endl;
// Demonstration of why naive recursion is slow
cout << "Why Naive Recursion is Slow for Fibonacci:" << endl;
cout << "fib(5) = fib(4) + fib(3)" << endl;
cout << "fib(4) = fib(3) + fib(2)" << endl;
cout << "fib(3) = fib(2) + fib(1)" << endl;
cout << "Notice fib(3) is calculated twice!" << endl;
cout << "For fib(30), same values are recalculated millions of times!" << endl;
return 0;
}
// ITERATIVE FIBONACCI (Most efficient)
int fibonacciIterative(int n) {
if (n < 0) return -1; // Error
if (n <= 1) return n;
int prev = 0, curr = 1;
for (int i = 2; i <= n; i++) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
// NAIVE RECURSIVE FIBONACCI (Inefficient - O(2^n))
int fibonacciRecursive(int n) {
if (n < 0) return -1; // Error
if (n <= 1) return n;
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
// MEMOIZATION (Optimized recursion - O(n))
int fibonacciMemoHelper(int n, vector<int>& memo) {
if (n <= 1) return n;
// If already calculated, return cached value
if (memo[n] != -1) {
return memo[n];
}
// Calculate and store in memo
memo[n] = fibonacciMemoHelper(n - 1, memo) + fibonacciMemoHelper(n - 2, memo);
return memo[n];
}
int fibonacciMemoization(int n) {
if (n < 0) return -1;
vector<int> memo(n + 1, -1); // Initialize with -1 (not calculated)
return fibonacciMemoHelper(n, memo);
}
// DYNAMIC PROGRAMMING (Bottom-up approach)
int fibonacciDP(int n) {
if (n < 0) return -1;
if (n <= 1) return n;
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
void printFibonacciComparison(int terms) {
cout << "n\tIterative\tRecursive\tMemoization\tDP" << endl;
cout << "-\t---------\t---------\t-----------\t--" << endl;
for (int i = 0; i < terms; i++) {
cout << i << "\t"
<< fibonacciIterative(i) << "\t\t"
<< fibonacciRecursive(i) << "\t\t"
<< fibonacciMemoization(i) << "\t\t"
<< fibonacciDP(i) << endl;
}
}
Iterative Factorial
- Time: O(n)
- Space: O(1)
- Uses simple loop
- No stack overhead
- Best for performance
Recursive Factorial
- Time: O(n)
- Space: O(n) for stack
- Elegant mathematical definition
- Risk of stack overflow for large n
- Slower due to function calls
Fibonacci Insights
- Naive recursion: O(2ⁿ) - exponential!
- Iterative/DP: O(n) - linear
- Memoization: O(n) with recursion
- Demonstrates importance of optimization
- Classic example of overlapping subproblems
2. Recursion Stack Visualization
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.
Stack Frame Anatomy for factorialRecursive(5)
#include <iostream>
#include <string>
using namespace std;
// Function to visualize recursion depth
void printIndent(int depth) {
for (int i = 0; i < depth; i++) {
cout << "| ";
}
}
// Factorial with stack visualization
long long factorialVisual(int n, int depth = 0) {
printIndent(depth);
cout << "→ factorial(" << n << ") called" << endl;
if (n < 0) {
printIndent(depth);
cout << "← ERROR: Negative number!" << endl;
throw invalid_argument("Negative number");
}
if (n <= 1) {
printIndent(depth);
cout << "← BASE CASE: returning 1" << endl;
return 1;
}
printIndent(depth);
cout << " Need to calculate " << n << " * factorial(" << n-1 << ")" << endl;
// Recursive call
long long smallerFact = factorialVisual(n - 1, depth + 1);
long long result = n * smallerFact;
printIndent(depth);
cout << "← factorial(" << n << ") = " << n << " * " << smallerFact
<< " = " << result << endl;
return result;
}
// Fibonacci with stack visualization (naive version)
int fibonacciVisual(int n, int depth = 0) {
printIndent(depth);
cout << "→ fibonacci(" << n << ") called" << endl;
if (n < 0) {
printIndent(depth);
cout << "← ERROR: Negative number!" << endl;
return -1;
}
if (n <= 1) {
printIndent(depth);
cout << "← BASE CASE: returning " << n << endl;
return n;
}
printIndent(depth);
cout << " Need fibonacci(" << n-1 << ") + fibonacci(" << n-2 << ")" << endl;
// Two recursive calls - demonstrates exponential growth
int fib1 = fibonacciVisual(n - 1, depth + 1);
printIndent(depth);
cout << " Got fibonacci(" << n-1 << ") = " << fib1
<< ", now need fibonacci(" << n-2 << ")" << endl;
int fib2 = fibonacciVisual(n - 2, depth + 1);
int result = fib1 + fib2;
printIndent(depth);
cout << "← fibonacci(" << n << ") = " << fib1 << " + " << fib2
<< " = " << result << endl;
return result;
}
int main() {
cout << "=== RECURSION STACK VISUALIZATION ===" << endl << endl;
// Example 1: Factorial visualization
cout << "Example 1: factorialVisual(5)" << endl;
cout << "Call stack visualization:" << endl;
cout << "==========================" << endl;
long long fact5 = factorialVisual(5);
cout << "==========================" << endl;
cout << "Final result: " << fact5 << endl;
cout << endl << "=====================================" << endl << endl;
// Example 2: Fibonacci visualization (small n)
cout << "Example 2: fibonacciVisual(4)" << endl;
cout << "Call stack visualization:" << endl;
cout << "==========================" << endl;
cout << "Note: Same values are recalculated!" << endl;
int fib4 = fibonacciVisual(4);
cout << "==========================" << endl;
cout << "Final result: " << fib4 << endl;
cout << endl << "=====================================" << endl << endl;
// Example 3: Stack overflow demonstration
cout << "Example 3: Stack Overflow Risk" << endl;
cout << "Trying factorialVisual(10000)..." << endl;
try {
// This will likely cause stack overflow
// long long hugeFact = factorialVisual(10000);
// cout << "Result: " << hugeFact << endl;
cout << "(Commented out to prevent crash)" << endl;
} catch (const exception& e) {
cout << "Error: " << e.what() << endl;
}
cout << endl << "Recursive calls create stack frames." << endl;
cout << "Each frame contains:" << endl;
cout << "1. Parameters" << endl;
cout << "2. Local variables" << endl;
cout << "3. Return address" << endl;
cout << "4. Previous frame pointer" << endl;
cout << endl << "Stack size is limited (typically 1-8 MB)" << endl;
return 0;
}
- Each recursive call uses stack memory (typically 1-8 MB total)
- Deep recursion can exhaust stack space
- Common with: No base case, incorrect base case, too deep recursion
- Symptoms: Program crashes with segmentation fault
- Solutions: Use iteration, increase stack size, optimize recursion
3. When to Use Recursion vs Iteration: Decision Guide
Choosing 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 |
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
using namespace std;
// Tree node structure
struct TreeNode {
int value;
TreeNode* left;
TreeNode* right;
TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};
// Example 1: Binary Tree Traversal
TreeNode* createSampleTree() {
// Create tree: 1
// / \
// 2 3
// / \ /
// 4 5 6
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
root->right->left = new TreeNode(6);
return root;
}
// Recursive In-order Traversal (Natural fit)
void inorderRecursive(TreeNode* node) {
if (node == nullptr) return;
inorderRecursive(node->left);
cout << node->value << " ";
inorderRecursive(node->right);
}
// Iterative In-order Traversal (More complex)
void inorderIterative(TreeNode* root) {
stack<TreeNode*> stk;
TreeNode* current = root;
while (current != nullptr || !stk.empty()) {
// Reach leftmost node
while (current != nullptr) {
stk.push(current);
current = current->left;
}
current = stk.top();
stk.pop();
cout << current->value << " ";
current = current->right;
}
}
// Example 2: Directory-like structure
struct Directory {
string name;
vector<Directory*> subdirectories;
vector<string> files;
Directory(string n) : name(n) {}
};
// Recursive directory traversal (Natural)
void listDirectoryRecursive(Directory* dir, int depth = 0) {
string indent(depth * 2, ' ');
cout << indent << dir->name << "/" << endl;
// List files
for (const string& file : dir->files) {
cout << indent << " " << file << endl;
}
// Recursively list subdirectories
for (Directory* subdir : dir->subdirectories) {
listDirectoryRecursive(subdir, depth + 1);
}
}
// Iterative directory traversal (Complex)
void listDirectoryIterative(Directory* root) {
stack<pair<Directory*, int>> stk;
stk.push({root, 0});
while (!stk.empty()) {
auto [current, depth] = stk.top();
stk.pop();
string indent(depth * 2, ' ');
cout << indent << current->name << "/" << endl;
// List files
for (const string& file : current->files) {
cout << indent << " " << file << endl;
}
// Push subdirectories in reverse order for correct traversal
for (auto it = current->subdirectories.rbegin();
it != current->subdirectories.rend(); ++it) {
stk.push({*it, depth + 1});
}
}
}
// Example 3: Simple array processing (Better with iteration)
int sumArrayIterative(const vector<int>& arr) {
int sum = 0;
for (int num : arr) {
sum += num;
}
return sum;
}
int sumArrayRecursive(const vector<int>& arr, int index = 0) {
if (index >= arr.size()) return 0;
return arr[index] + sumArrayRecursive(arr, index + 1);
}
int main() {
cout << "=== REAL-WORLD RECURSION vs ITERATION EXAMPLES ===" << endl << endl;
// Example 1: Tree Traversal
cout << "Example 1: Binary Tree In-order Traversal" << endl;
TreeNode* tree = createSampleTree();
cout << "Recursive: ";
inorderRecursive(tree);
cout << endl;
cout << "Iterative: ";
inorderIterative(tree);
cout << endl;
cout << "Verdict: Recursion is cleaner for tree traversal" << endl;
cout << endl << "===========================================" << endl << endl;
// Example 2: Directory Structure
cout << "Example 2: Directory Structure Traversal" << endl;
Directory* root = new Directory("root");
root->files = {"readme.txt", "config.ini"};
Directory* docs = new Directory("documents");
docs->files = {"report.pdf", "notes.txt"};
root->subdirectories.push_back(docs);
Directory* pics = new Directory("pictures");
pics->files = {"photo1.jpg", "photo2.jpg"};
root->subdirectories.push_back(pics);
Directory* vacation = new Directory("vacation");
vacation->files = {"beach.jpg", "mountain.png"};
pics->subdirectories.push_back(vacation);
cout << "Recursive traversal:" << endl;
listDirectoryRecursive(root);
cout << endl << "Iterative traversal:" << endl;
listDirectoryIterative(root);
cout << "Verdict: Recursion is more natural for hierarchical structures" << endl;
cout << endl << "===========================================" << endl << endl;
// Example 3: Array Processing
cout << "Example 3: Array Summation" << endl;
vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "Iterative sum: " << sumArrayIterative(numbers) << endl;
cout << "Recursive sum: " << sumArrayRecursive(numbers) << endl;
cout << "Verdict: Iteration is simpler and more efficient for linear arrays" << endl;
cout << endl << "===========================================" << endl << endl;
// Cleanup
// Note: In real code, properly delete allocated memory
cout << "Decision Guidelines:" << endl;
cout << "1. Use RECURSION for:" << endl;
cout << " • Tree/graph traversal" << endl;
cout << " • Divide and conquer algorithms" << endl;
cout << " • Backtracking problems" << endl;
cout << " • Hierarchical data structures" << endl;
cout << endl;
cout << "2. Use ITERATION for:" << endl;
cout << " • Simple loops/linear processing" << endl;
cout << " • Performance-critical code" << endl;
cout << " • When stack depth could be large" << endl;
cout << " • When recursion doesn't simplify code" << endl;
return 0;
}
4. Tail Recursion Optimization
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.
Tail Recursion Pattern
// 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
}
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
// Regular recursion (NOT tail recursive)
int sumRegular(int n) {
if (n <= 0) return 0;
return n + sumRegular(n - 1); // Addition happens AFTER return
}
// Tail recursive version
int sumTail(int n, int accumulator = 0) {
if (n <= 0) return accumulator;
return sumTail(n - 1, accumulator + n); // Recursive call is LAST operation
}
// Regular factorial (NOT tail recursive)
long long factorialRegular(int n) {
if (n <= 1) return 1;
return n * factorialRegular(n - 1); // Multiplication AFTER call
}
// Tail recursive factorial
long long factorialTail(int n, long long accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // Call is LAST operation
}
// GCD using Euclid's algorithm (naturally tail recursive)
int gcdRegular(int a, int b) {
if (b == 0) return a;
return gcdRegular(b, a % b); // Naturally tail recursive!
}
// Fibonacci tail recursive (accumulator pattern)
int fibonacciTail(int n, int a = 0, int b = 1) {
if (n == 0) return a;
if (n == 1) return b;
return fibonacciTail(n - 1, b, a + b);
}
// Helper function to test stack usage
void testDeepRecursion(int n, int& maxDepth, int currentDepth = 1) {
maxDepth = max(maxDepth, currentDepth);
if (n <= 0) return;
// Force deep recursion to test stack limits
testDeepRecursion(n - 1, maxDepth, currentDepth + 1);
}
int main() {
cout << "=== TAIL RECURSION OPTIMIZATION ===" << endl << endl;
// Example 1: Sum of first n numbers
cout << "Example 1: Sum of first 100 numbers" << endl;
cout << "Regular recursion: " << sumRegular(100) << endl;
cout << "Tail recursion: " << sumTail(100) << endl;
cout << "Mathematical: " << (100 * 101) / 2 << endl;
cout << endl << "=================================" << endl << endl;
// Example 2: Factorial comparison
cout << "Example 2: Factorial of 10" << endl;
cout << "Regular: " << factorialRegular(10) << endl;
cout << "Tail: " << factorialTail(10) << endl;
cout << endl << "=================================" << endl << endl;
// Example 3: GCD (naturally tail recursive)
cout << "Example 3: GCD of 48 and 18" << endl;
cout << "GCD(48, 18) = " << gcdRegular(48, 18) << endl;
cout << endl << "=================================" << endl << endl;
// Example 4: Fibonacci with tail recursion
cout << "Example 4: Fibonacci(10) with different approaches" << endl;
cout << "Naive recursive: Too slow for demonstration" << endl;
cout << "Tail recursive: " << fibonacciTail(10) << endl;
cout << endl << "=================================" << endl << endl;
// Performance comparison
cout << "Performance Comparison (n=10000 for sum):" << endl;
// WARNING: Regular recursion may cause stack overflow!
cout << "Note: Regular recursion with n=10000 may crash!" << endl;
// Test with smaller number to avoid crash
int testN = 1000;
auto start = high_resolution_clock::now();
int tailResult = sumTail(testN);
auto stop = high_resolution_clock::now();
auto tailTime = duration_cast<microseconds>(stop - start);
cout << "Tail recursion (n=" << testN << "): " << tailTime.count()
<< " microseconds" << endl;
// Regular recursion with smaller number to avoid stack overflow
testN = 500; // Smaller to prevent crash
start = high_resolution_clock::now();
int regularResult = sumRegular(testN);
stop = high_resolution_clock::now();
auto regularTime = duration_cast<microseconds>(stop - start);
cout << "Regular recursion (n=" << testN << "): " << regularTime.count()
<< " microseconds" << endl;
cout << endl << "=================================" << endl << endl;
// How tail recursion optimization works
cout << "How Tail Recursion Optimization Works:" << endl;
cout << "1. Regular recursion:" << endl;
cout << " sum(5) = 5 + sum(4)" << endl;
cout << " sum(4) = 4 + sum(3)" << endl;
cout << " ... Each call waits for result" << endl;
cout << endl;
cout << "2. Tail recursion (after optimization):" << endl;
cout << " sumTail(5, 0)" << endl;
cout << " sumTail(4, 5)" << endl;
cout << " sumTail(3, 9)" << endl;
cout << " ... Current stack frame REUSED!" << endl;
cout << endl;
cout << "Compiler optimization (tail call elimination):" << endl;
cout << "• Instead of new stack frame, reuse current" << endl;
cout << "• Update parameters and jump to start" << endl;
cout << "• Essentially becomes a loop!" << endl;
cout << endl << "=================================" << endl << endl;
// Testing stack depth
cout << "Testing Maximum Recursion Depth:" << endl;
int maxDepth = 0;
// Test with safe value
testDeepRecursion(100, maxDepth);
cout << "Maximum recursion depth reached: " << maxDepth << endl;
cout << "Typical stack limits: 1MB to 8MB" << endl;
cout << "Each call uses ~100-500 bytes" << endl;
cout << "Max safe depth: ~1000-8000 calls" << endl;
return 0;
}
- Recursive call must be the LAST operation in the function
- No operations should happen after the recursive call
- Use accumulator parameters to carry results forward
- Modern C++ compilers (GCC, Clang) optimize tail recursion with -O2 or -O3
- MSVC may not optimize all tail recursion cases
- Always check if your compiler optimizes tail recursion
5. Best Practices and Common Mistakes
Recursion Best Practices
- Always define a base case first
- Ensure progress toward base case
- Use tail recursion when possible
- Consider memoization for overlapping subproblems
- Test with edge cases (0, 1, negative, large values)
- Document recursion depth expectations
- Consider iterative alternatives for performance
Common Recursion Mistakes
- Missing base case (infinite recursion)
- Base case never reached (incorrect progress)
- Forgetting return statement in recursive case
- Stack overflow with deep recursion
- Exponential time complexity (Fibonacci naive)
- Using recursion where iteration is better
- Not considering memoization for repeated calculations
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// ===== BAD PRACTICES =====
// BAD: Missing base case (will crash)
int badFactorial(int n) {
// Missing: if (n <= 1) return 1;
return n * badFactorial(n - 1); // Infinite recursion!
}
// BAD: Incorrect base case (may never reach)
int badSum(int n) {
if (n == 1) return 1; // What about n=0? n=negative?
return n + badSum(n - 1);
}
// BAD: No progress toward base case
int infiniteRecursion(int n) {
if (n == 0) return 0;
return infiniteRecursion(n); // Same n, never decreases!
}
// BAD: Using recursion for simple iteration
void printNumbersBad(int n) {
if (n <= 0) return;
printNumbersBad(n - 1);
cout << n << " ";
// Simple loop would be better: for(int i=1; i<=n; i++) cout< 0) {
sum += n % 10;
n /= 10;
}
return sum;
}
// GOOD: Using recursion for natural problems
struct FileNode {
string name;
bool isDirectory;
vector<FileNode*> children;
FileNode(string n, bool dir) : name(n), isDirectory(dir) {}
};
// Natural use of recursion: File system traversal
void listFilesRecursive(FileNode* node, int depth = 0) {
string indent(depth * 2, ' ');
cout << indent << node->name << (node->isDirectory ? "/" : "") << endl;
if (node->isDirectory) {
for (FileNode* child : node->children) {
listFilesRecursive(child, depth + 1);
}
}
}
int main() {
cout << "=== RECURSION: GOOD vs BAD PRACTICES ===" << endl << endl;
// Good practices demonstration
cout << "Good Practices Examples:" << endl;
cout << "1. Proper factorial(5): " << goodFactorial(5) << endl;
cout << "2. Tail recursive factorial(5): " << goodFactorialTail(5) << endl;
cout << "3. Sum of digits(12345): " << sumDigits(12345) << endl;
cout << "4. Iterative sum of digits(12345): " << sumDigitsIterative(12345) << endl;
cout << endl << "Fibonacci with Memoization (n=40):" << endl;
cout << "This would be impossible with naive recursion!" << endl;
cout << "fibonacciMemoized(40) = " << fibonacciMemoized(40) << endl;
cout << endl << "=====================================" << endl << endl;
// Creating a sample file structure
cout << "Natural Recursion Example: File System" << endl;
FileNode* root = new FileNode("root", true);
FileNode* docs = new FileNode("documents", true);
FileNode* pics = new FileNode("pictures", true);
root->children.push_back(docs);
root->children.push_back(pics);
root->children.push_back(new FileNode("readme.txt", false));
docs->children.push_back(new FileNode("report.pdf", false));
docs->children.push_back(new FileNode("notes.txt", false));
pics->children.push_back(new FileNode("photo1.jpg", false));
pics->children.push_back(new FileNode("photo2.jpg", false));
FileNode* vacation = new FileNode("vacation", true);
vacation->children.push_back(new FileNode("beach.jpg", false));
pics->children.push_back(vacation);
cout << "File structure:" << endl;
listFilesRecursive(root);
cout << endl << "=====================================" << endl << endl;
cout << "Recursion Guidelines Summary:" << endl;
cout << "✓ DO use recursion for:" << endl;
cout << " • Tree/graph traversal" << endl;
cout << " • Divide and conquer" << endl;
cout << " • Backtracking problems" << endl;
cout << " • When it simplifies code significantly" << endl;
cout << endl;
cout << "✓ DO NOT use recursion for:" << endl;
cout << " • Simple linear iteration" << endl;
cout << " • Performance-critical code" << endl;
cout << " • When recursion depth could be large" << endl;
cout << " • When iterative solution is equally clear" << endl;
cout << endl;
cout << "✓ ALWAYS:" << endl;
cout << " • Define a base case" << endl;
cout << " • Ensure progress toward base case" << endl;
cout << " • Consider stack limitations" << endl;
cout << " • Test with edge cases" << endl;
cout << " • Consider memoization for repeated calculations" << endl;
// Cleanup (in real code, use smart pointers)
delete root;
delete docs;
delete pics;
return 0;
}