Using Recursion Patterns
1. Understanding Recursion Fundamentals
| Concept | Description | Key Properties |
|---|---|---|
| Base Case | The condition that stops the recursion | Ensures termination, prevents infinite loops |
| Recursive Case | The function calling itself with modified parameters | Progresses toward base case, maintains problem structure |
| Call Stack | Memory structure storing function calls during execution | Tracks local variables, return addresses |
Example: Factorial Calculation
function factorial(n) {
// Base case
if (n === 0 || n === 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}
console.log(factorial(5)); // Output: 120
Note: Each recursive call adds a new frame to the call stack until the base case is reached.
Recursion Execution Flow
- Function is called with input parameter
- Check if base case is met
- If not, make recursive call with updated parameter
- Continue until base case is reached
- Return values propagate back up the call stack
Core Principles of Recursion
- Must have a well-defined base case to prevent infinite recursion
- Each recursive call must move closer to the base case
- The problem should be divisible into smaller subproblems of the same type
- Recursive solutions often mirror mathematical definitions
2. Implementing Tail Recursion Optimization
| Aspect | Description | Notes |
|---|---|---|
| Definition | Tail recursion occurs when the recursive call is the last operation in the function. | Optimization can convert tail recursion into iteration. |
| Example (JavaScript) | |
Accumulator pattern used to pass state forward. |
| Optimization Potential | Some compilers/interpreters optimize tail recursion into loops. | Not guaranteed in all environments (e.g., JavaScript engines). |
Example: Tail Recursion vs Regular Recursion
// Regular recursion (not tail-optimized)
function factorial(n) {
if (n === 0) return 1;
return n * factorial(n - 1);
}
// Tail-recursive version
function factorialTail(n, acc = 1) {
if (n === 0) return acc;
return factorialTail(n - 1, n * acc);
}
Note: While tail recursion is a powerful pattern, JavaScript engines do not always optimize it. For performance-critical code, consider iterative alternatives.
3. Converting Recursion to Iteration
| Recursion Pattern | Iteration Equivalent | Notes |
|---|---|---|
| Tail Recursion | while (condition) { ... } |
Can be directly converted to a loop without stack buildup |
| Tree Traversal (Post-order) | Stack-based traversal with explicit state tracking | Use a stack to simulate the call stack behavior |
| Divide and Conquer | Iterative divide with loop control | Simulate recursive splits using a queue or stack |
Example: Converting Tail Recursion to Loop
// Recursive version
function sumRecursive(n, acc = 0) {
if (n === 0) return acc;
return sumRecursive(n - 1, acc + n);
}
// Iterative version
function sumIterative(n) {
let acc = 0;
while (n > 0) {
acc += n;
n--;
}
return acc;
}
Example: Tree Traversal (Post-order)
// Recursive version
function postOrder(node) {
if (!node) return;
postOrder(node.left);
postOrder(node.right);
console.log(node.value);
}
// Iterative version using stack
function postOrderIterative(root) {
const stack = [root];
const result = [];
while (stack.length > 0) {
const node = stack.pop();
if (node) {
result.push(node.value);
stack.push(node.left);
stack.push(node.right);
}
}
// Reverse to get post-order
return result.reverse();
}
Note: For complex recursive patterns like binary tree traversals, the iterative version requires careful handling of state and may involve additional data structures like stacks or queues.
4. Calculating Factorials Recursively
| Method | Description | Code Example | Notes |
|---|---|---|---|
| Base Case | Factorial of 0 or 1 is 1 | n === 0 || n === 1 |
Stops the recursion |
| Recursive Case | Factorial of n is n * factorial(n - 1) | n * factorial(n - 1) |
Breaks problem into smaller subproblems |
Example: Calculating Factorial
function factorial(n) {
if (n === 0 || n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(5)); // Output: 120
Note: For large values of n, recursion can lead to stack overflow errors. Consider iterative approaches for better performance in such cases.
5. Generating Fibonacci Numbers
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Recursive | Basic recursive implementation without memoization | O(2^n) |
O(n) |
| Memoized Recursive | Recursive with caching of previously computed values | O(n) |
O(n) |
| Iterative | Bottom-up approach using loop and variables | O(n) |
O(1) |
| Matrix Exponentiation | Using matrix multiplication to compute Fibonacci numbers | O(log n) |
O(log n) |
Example: Memoized Recursive Implementation
const fibMemo = (n, memo = {}) => {
if (n in memo) return memo[n];
if (n <= 1) return n;
memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
return memo[n];
};
console.log(fibMemo(10)); // 55
Example: Iterative Implementation
function fibIterative(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
console.log(fibIterative(10)); // 55
Example: Matrix Exponentiation Implementation
function matrixMultiply(A, B) {
return [
[A[0][0]*B[0][0] + A[0][1]*B[1][0], A[0][0]*B[0][1] + A[0][1]*B[1][1]],
[A[1][0]*B[0][0] + A[1][1]*B[1][0], A[1][0]*B[0][1] + A[1][1]*B[1][1]]
];
}
function matrixPower(matrix, n) {
if (n === 1) return matrix;
if (n % 2 === 0) {
const half = matrixPower(matrix, n / 2);
return matrixMultiply(half, half);
} else {
return matrixMultiply(matrix, matrixPower(matrix, n - 1));
}
}
function fibMatrix(n) {
if (n <= 1) return n;
const baseMatrix = [[1, 1], [1, 0]];
const resultMatrix = matrixPower(baseMatrix, n);
return resultMatrix[0][1];
}
console.log(fibMatrix(10)); // 55
6. Computing Power Function
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Iterative | Uses a loop to multiply the base by itself n times. | O(n) |
O(1) |
| Recursive | Recursively computes power by dividing exponent in half. | O(log n) |
O(log n) |
| Fast Exponentiation (Binary) | Uses binary representation of exponent to reduce operations. | O(log n) |
O(log n) |
Example: Recursive Power Function
function power(base, exp) {
if (exp === 0) return 1;
if (exp === 1) return base;
const half = power(base, Math.floor(exp / 2));
if (exp % 2 === 0) {
return half * half;
} else {
return base * half * half;
}
}
// Example usage:
console.log(power(2, 10)); // Output: 1024
Example: Iterative Power Function
function powerIterative(base, exp) {
let result = 1;
while (exp > 0) {
if (exp % 2 === 1) {
result *= base;
}
base *= base;
exp = Math.floor(exp / 2);
}
return result;
}
// Example usage:
console.log(powerIterative(3, 4)); // Output: 81
7. Calculating GCD
The Greatest Common Divisor (GCD) of two numbers is the largest positive integer that divides both numbers without remainder. The Euclidean algorithm is a classic recursive approach to compute GCD.
| Method | Description | Implementation | Notes |
|---|---|---|---|
| Recursive Euclidean Algorithm | Repeatedly applies the property that GCD(a, b) = GCD(b, a mod b) | |
Base case: when b is 0, return a |
| Iterative Euclidean Algorithm | Same logic but implemented with a loop instead of recursion | |
More memory efficient for large inputs |
Example: Calculating GCD of 48 and 18
gcd(48, 18) // Returns 6
// Step-by-step:
// gcd(48, 18) → gcd(18, 48 % 18) → gcd(18, 12)
// gcd(18, 12) → gcd(12, 18 % 12) → gcd(12, 6)
// gcd(12, 6) → gcd(6, 12 % 6) → gcd(6, 0)
// Returns 6
Note: The recursive approach is more intuitive and mirrors the mathematical definition closely.
Warning: For very large numbers, recursion depth might exceed limits in some environments.
8. Reversing Strings Recursively
| Method | Description | Example |
|---|---|---|
| Base Case | When string length is 0 or 1, return the string itself | if (str.length <= 1) return str; |
| Recursive Case | Take first character and append it to the end of reversed rest | return reverse(str.slice(1)) + str[0]; |
Example: Reverse String Using Recursion
function reverseString(str) {
if (str.length <= 1) return str;
return reverseString(str.slice(1)) + str[0];
}
console.log(reverseString("hello")); // "olleh"
Note: This approach has O(n²) time complexity due to string concatenation in JavaScript.
9. Implementing Tree Traversals
| Traversal Type | Order of Visits | Code Example | Use Case |
|---|---|---|---|
| Inorder | Left → Root → Right | |
Sorted output for BST |
| Preorder | Root → Left → Right | |
Copy tree structure |
| Postorder | Left → Right → Root | |
Calculate sizes, delete nodes |
Example: Inorder Traversal of BST
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
let root = new TreeNode(10);
root.left = new TreeNode(5);
root.right = new TreeNode(15);
root.left.right = new TreeNode(7);
function inorder(node) {
if (node) {
inorder(node.left);
console.log(node.value); // 5, 7, 10, 15
inorder(node.right);
}
}
inorder(root);
10. Generating All Subsets
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Recursive | Generate subsets by including or excluding each element recursively. | O(2^n) |
O(n) |
| Iterative | Start with an empty subset and iteratively add elements to existing subsets. | O(2^n) |
O(1) (excluding output) |
| Bit Manipulation | Use bitmasks to represent subset inclusion; iterate from 0 to 2^n - 1. | O(n * 2^n) |
O(1) (excluding output) |
Example: Recursive Approach
function generateSubsets(nums) {
const result = [];
function backtrack(start, currentSubset) {
result.push([...currentSubset]);
for (let i = start; i < nums.length; i++) {
currentSubset.push(nums[i]);
backtrack(i + 1, currentSubset);
currentSubset.pop();
}
}
backtrack(0, []);
return result;
}
// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
Example: Iterative Approach
function generateSubsets(nums) {
let result = [[]];
for (const num of nums) {
const newSubsets = [];
for (const subset of result) {
newSubsets.push([...subset, num]);
}
result = [...result, ...newSubsets];
}
return result;
}
// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
Example: Bit Manipulation Approach
function generateSubsets(nums) {
const result = [];
const n = nums.length;
const totalSubsets = 1 << n;
for (let i = 0; i < totalSubsets; i++) {
const subset = [];
for (let j = 0; j < n; j++) {
if (i & (1 << j)) {
subset.push(nums[j]);
}
}
result.push(subset);
}
return result;
}
// Example usage:
const nums = [1, 2, 3];
console.log(generateSubsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
11. Implementing Merge Sort
Merge Sort is a divide-and-conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves.
| Property | Description | Example |
|---|---|---|
| Time Complexity | Best, Average, and Worst case: O(n log n) | O(n log n) |
| Space Complexity | O(n) due to auxiliary space for merging | O(n) |
| Stability | Stable sorting algorithm | Yes |
| Paradigm | Divide and Conquer | Divide, conquer, combine |
Example: Merge Sort Implementation in JavaScript
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
let result = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
// Example usage:
const array = [38, 27, 43, 3, 9, 82, 10];
console.log(mergeSort(array)); // [3, 9, 10, 27, 38, 43, 82]
Note: Merge Sort is stable and guarantees O(n log n) performance in all cases, making it ideal for applications where consistent performance is required.
12. Solving Tower of Hanoi Problem
The Tower of Hanoi is a classic recursive problem that involves moving disks between rods according to specific rules.
| Parameter | Description | Values |
|---|---|---|
| n | Number of disks | int |
| source | Starting rod | string |
| destination | Target rod | string |
| auxiliary | Helper rod | string |
Example: Recursive Solution
function hanoi(n, source, destination, auxiliary) {
if (n === 1) {
console.log(`Move disk from ${source} to ${destination}`);
return;
}
hanoi(n - 1, source, auxiliary, destination);
console.log(`Move disk from ${source} to ${destination}`);
hanoi(n - 1, auxiliary, destination, source);
}
hanoi(3, 'A', 'C', 'B');
Note: The recursive approach breaks the problem into smaller subproblems. For n disks, it takes 2^n - 1 moves.