Skip to content

Instantly share code, notes, and snippets.

@decagondev
Last active August 6, 2025 22:14
Show Gist options
  • Select an option

  • Save decagondev/6fefa132ba4dd1519f78d643a64a3c61 to your computer and use it in GitHub Desktop.

Select an option

Save decagondev/6fefa132ba4dd1519f78d643a64a3c61 to your computer and use it in GitHub Desktop.

Deep Dive into Recursion: Concepts, Algorithms, and Use Cases

This document provides an in-depth exploration of recursion, a programming technique where a function calls itself to solve smaller instances of a problem. We cover its concepts, mechanics, algorithms, visualization with Mermaid diagrams (using correct syntax), and implementation in JavaScript with examples. Additionally, we explore three practical use cases, compare recursion with iteration, and discuss optimization techniques like tail recursion and memoization.

Recursion Overview

Recursion involves a function solving a problem by breaking it into smaller subproblems of the same type, each solved by calling the function again. It relies on:

  • Base Case: A condition that stops recursion to prevent infinite calls.
  • Recursive Case: The part where the function calls itself with a modified input.

Properties

  • Structure: Each recursive call reduces the problem size, converging toward the base case.
  • Stack Usage: Each call adds a frame to the call stack, consuming memory (O(n) space for n recursive calls).
  • Time Complexity: Depends on the number of recursive calls and work per call (e.g., O(2^n) for naive Fibonacci, O(n) for factorial).
  • Advantages:
    • Elegant solutions for problems with recursive structures (e.g., trees, graphs).
    • Simplifies code for divide-and-conquer algorithms.
  • Disadvantages:
    • Stack overflow for deep recursion.
    • Potential performance overhead compared to iteration.

Use Cases

  • Tree and graph traversals.
  • Divide-and-conquer algorithms (e.g., merge sort, quicksort).
  • Combinatorial problems (e.g., permutations, subsets).
  • Mathematical computations (e.g., factorial, Fibonacci).

Recursion Mechanics

Recursion can be visualized as a stack of function calls, where each call waits for its subcalls to complete before returning.

Mermaid Diagram (Recursion Call Stack)

graph TD
    A[Function Call: n] --> B{Base Case?}
    B -->|Yes| C[Return Result]
    B -->|No| D[Recursive Call: n-1]
    D --> E[Recursive Call: n-2]
    E --> F[More Recursive Calls]
    F --> G[Base Case Reached]
    G --> H[Unwind Stack, Combine Results]
    H --> I[Return Final Result]
Loading

Core Recursive Algorithms

Factorial

Concept

Computes n! (n factorial), the product of all positive integers up to n (e.g., 5! = 5 × 4 × 3 × 2 × 1 = 120).

  • Base Case: n = 0 or 1, return 1.
  • Recursive Case: n! = n × (n-1)!.

Algorithm

  1. If n <= 1, return 1.
  2. Return n × factorial(n-1).

Mermaid Diagram

graph TD
    A[Start Factorial: n] --> B{Is n <= 1?}
    B -->|Yes| C[Return 1]
    B -->|No| D[Return n times Factorial n-1]
    D --> E[Factorial: n-1]
    E --> F[More Recursive Calls]
    F --> G[Base Case: Return 1]
    G --> H[Unwind: Compute Products]
    H --> I[Return Result]
Loading

Implementation

function factorial(n) {
  if (n <= 1) return 1; // Base case
  return n * factorial(n - 1); // Recursive case
}

// Example usage
console.log(factorial(5)); // Output: 120
console.log(factorial(3)); // Output: 6

Fibonacci Sequence

Concept

Computes the nth Fibonacci number, where Fib(n) = Fib(n-1) + Fib(n-2), and Fib(0) = 0, Fib(1) = 1.

  • Base Case: n = 0 returns 0, n = 1 returns 1.
  • Recursive Case: Fib(n) = Fib(n-1) + Fib(n-2).

Algorithm

  1. If n = 0, return 0.
  2. If n = 1, return 1.
  3. Return Fib(n-1) + Fib(n-2).

Mermaid Diagram

graph TD
    A[Start Fibonacci: n] --> B{Is n = 0?}
    B -->|Yes| C[Return 0]
    B -->|No| D{Is n = 1?}
    D -->|Yes| E[Return 1]
    D -->|No| F[Return Fib n-1 + Fib n-2]
    F --> G[Fib: n-1]
    F --> H[Fib: n-2]
    G --> I[More Recursive Calls]
    H --> J[More Recursive Calls]
    I --> K[Base Case]
    J --> L[Base Case]
    K --> M[Unwind: Sum Results]
    L --> M
    M --> N[Return Result]
Loading

Implementation

function fibonacci(n) {
  if (n === 0) return 0; // Base case
  if (n === 1) return 1; // Base case
  return fibonacci(n - 1) + fibonacci(n - 2); // Recursive case
}

// Example usage
console.log(fibonacci(6)); // Output: 8 (0, 1, 1, 2, 3, 5, 8)
console.log(fibonacci(4)); // Output: 3

Optimization Techniques

Tail Recursion

Concept

In tail recursion, the recursive call is the last operation in the function, allowing some languages to optimize by reusing the same stack frame. JavaScript engines (e.g., V8) do not optimize tail recursion, but we can simulate it with an iterative approach or use an accumulator.

Example: Tail Recursive Factorial

function factorialTail(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTail(n - 1, n * accumulator);
}

// Example usage
console.log(factorialTail(5)); // Output: 120

Memoization

Concept

Memoization caches results of expensive recursive calls to avoid redundant computations, improving time complexity (e.g., Fibonacci from O(2^n) to O(n)).

Example: Memoized Fibonacci

function fibonacciMemoized() {
  const cache = {};
  function fib(n) {
    if (n in cache) return cache[n];
    if (n === 0) return 0;
    if (n === 1) return 1;
    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  }
  return fib;
}

// Example usage
const fib = fibonacciMemoized();
console.log(fib(6)); // Output: 8
console.log(fib(50)); // Output: 12586269025 (much faster than naive)

Use Cases

1. Tree Traversal (In-Order)

Concept

Performs an in-order traversal of a binary tree (left, root, right) to retrieve nodes in sorted order for a binary search tree (BST).

Algorithm

  1. If node is null, return.
  2. Recursively traverse left subtree.
  3. Process current node.
  4. Recursively traverse right subtree.

Mermaid Diagram

graph TD
    A[Start In-Order: node] --> B{Is node null?}
    B -->|Yes| C[Return]
    B -->|No| D[Traverse left: node.left]
    D --> E[Process node]
    E --> F[Traverse right: node.right]
    F --> G[Return]
Loading

Implementation

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

function inOrderTraversal(root) {
  const result = [];
  function traverse(node) {
    if (node) {
      traverse(node.left); // Left
      result.push(node.value); // Root
      traverse(node.right); // Right
    }
  }
  traverse(root);
  return result;
}

// Example usage
const tree = new Node(10);
tree.left = new Node(5);
tree.right = new Node(15);
tree.left.left = new Node(3);
tree.left.right = new Node(7);
console.log(inOrderTraversal(tree)); // Output: [3, 5, 7, 10, 15]

2. Permutations of a String

Concept

Generates all possible permutations of a string by recursively swapping characters.

Algorithm

  1. Base case: If string length is 1, return [string].
  2. For each character:
    • Take it as the first character.
    • Recursively generate permutations of the remaining characters.
    • Combine the first character with each permutation.
  3. Return all permutations.

Mermaid Diagram

graph TD
    A[Start Permute: str] --> B{Is str length 1?}
    B -->|Yes| C[Return str]
    B -->|No| D[For each char in str]
    D --> E[Take char as first]
    E --> F[Get remaining chars]
    F --> G[Recurse on remaining]
    G --> H[Combine char with permutations]
    H --> D
    D --> I[Return all permutations]
Loading

Implementation

function permute(str) {
  if (str.length <= 1) return [str];
  const result = [];
  for (let i = 0; i < str.length; i++) {
    const char = str[i];
    const remaining = str.slice(0, i) + str.slice(i + 1);
    const subPerms = permute(remaining);
    for (const sub of subPerms) {
      result.push(char + sub);
    }
  }
  return result;
}

// Example usage
console.log(permute("abc")); // Output: ["abc", "acb", "bac", "bca", "cab", "cba"]

3. Tower of Hanoi

Concept

Solves the Tower of Hanoi puzzle, moving n disks from source to destination via an auxiliary peg, following rules: only one disk moves at a time, and a larger disk cannot be placed on a smaller one.

Algorithm

  1. Base case: If n = 1, move disk from source to destination.
  2. Recursive case:
    • Move n-1 disks from source to auxiliary.
    • Move nth disk from source to destination.
    • Move n-1 disks from auxiliary to destination.

Mermaid Diagram

graph TD
    A[Start Hanoi: n, src, aux, dest] --> B{Is n = 1?}
    B -->|Yes| C[Move disk from src to dest]
    B -->|No| D[Hanoi: n-1, src, dest, aux]
    D --> E[Move nth disk from src to dest]
    E --> F[Hanoi: n-1, aux, src, dest]
    F --> G[Return]
Loading

Implementation

function towerOfHanoi(n, source, auxiliary, destination) {
  const moves = [];
  function hanoi(n, src, aux, dest) {
    if (n === 1) {
      moves.push(`Move disk 1 from ${src} to ${dest}`);
      return;
    }
    hanoi(n - 1, src, dest, aux);
    moves.push(`Move disk ${n} from ${src} to ${dest}`);
    hanoi(n - 1, aux, src, dest);
  }
  hanoi(n, source, auxiliary, destination);
  return moves;
}

// Example usage
console.log(towerOfHanoi(2, 'A', 'B', 'C'));
// Output: [
//   "Move disk 1 from A to B",
//   "Move disk 2 from A to C",
//   "Move disk 1 from B to C"
// ]

Recursion vs. Iteration

Comparison

  • Recursion:
    • Pros: Elegant, mirrors problem structure (e.g., trees), easier for divide-and-conquer.
    • Cons: Stack overflow risk, memory overhead, slower due to function calls.
  • Iteration:
    • Pros: More memory-efficient (O(1) space), faster due to no call overhead.
    • Cons: Can be less intuitive for naturally recursive problems.
  • Example (Factorial):
    // Recursive
    function factorial(n) {
      if (n <= 1) return 1;
      return n * factorial(n - 1);
    }
    
    // Iterative
    function factorialIterative(n) {
      let result = 1;
      for (let i = 1; i <= n; i++) {
        result *= i;
      }
      return result;
    }
    
    console.log(factorial(5)); // Output: 120
    console.log(factorialIterative(5)); // Output: 120

When to Use Recursion

  • Use recursion for problems with natural recursive structure (e.g., trees, graphs, permutations).
  • Use iteration for simple problems or when stack space is a concern.

Summary

  • Core Concepts:
    • Base case stops recursion; recursive case breaks problem into smaller instances.
    • Stack-based execution: Each call adds a frame until base case is reached.
  • Optimizations:
    • Tail Recursion: Reduces stack usage (not optimized in JavaScript).
    • Memoization: Caches results for efficiency (e.g., Fibonacci O(n)).
  • Use Cases:
    • Tree Traversal: In-order traversal for sorted BST output.
    • Permutations: Generates all possible arrangements.
    • Tower of Hanoi: Solves disk-moving puzzle recursively.
  • Time/Space:
    • Factorial: O(n) time, O(n) space.
    • Fibonacci (naive): O(2^n) time, O(n) space; memoized: O(n) time, O(n) space.
    • Tree Traversal: O(n) time, O(h) space (h = tree height).
    • Permutations: O(n!) time, O(n) space.
    • Tower of Hanoi: O(2^n) time, O(n) space.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment