Recursion - Complete Guide for Beginners

Recursion is a programming technique where a function calls itself to solve smaller instances of the same problem. This guide covers recursion fundamentals and common patterns.

What is Recursion? #

Recursion occurs when a function calls itself to break down a problem into smaller, more manageable subproblems.

Key Components:

  1. Base case - Stopping condition
  2. Recursive case - Function calls itself with modified parameters

Basic Recursion Example #

Countdown #

function countdown(n) {
  // Base case
  if (n <= 0) {
    console.log("Done!");
    return;
  }

  // Recursive case
  console.log(n);
  countdown(n - 1);
}

countdown(5);
// Output: 5, 4, 3, 2, 1, Done!

How Recursion Works #

Call Stack #

function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

factorial(3);

Call Stack Visualization:

factorial(3)
  3 * factorial(2)
        2 * factorial(1)
              1 * factorial(0)
                    return 1
              return 1 * 1 = 1
        return 2 * 1 = 2
  return 3 * 2 = 6

Classic Recursion Problems #

Factorial #

function factorial(n) {
  if (n === 0 || n === 1) {
    return 1;
  }
  return n * factorial(n - 1);
}

console.log(factorial(5));  // 120
// 5! = 5 × 4 × 3 × 2 × 1 = 120

Fibonacci #

function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(6));  // 8
// Sequence: 0, 1, 1, 2, 3, 5, 8

Sum of Array #

function sumArray(arr) {
  if (arr.length === 0) {
    return 0;
  }
  return arr[0] + sumArray(arr.slice(1));
}

console.log(sumArray([1, 2, 3, 4, 5]));  // 15

Power #

function power(base, exponent) {
  if (exponent === 0) {
    return 1;
  }
  return base * power(base, exponent - 1);
}

console.log(power(2, 5));  // 32

Reverse String #

function reverseString(str) {
  if (str === "") {
    return "";
  }
  return reverseString(str.slice(1)) + str[0];
}

console.log(reverseString("hello"));  // "olleh"

Array and String Recursion #

Count Occurrences #

function countOccurrences(arr, target) {
  if (arr.length === 0) {
    return 0;
  }

  const count = arr[0] === target ? 1 : 0;
  return count + countOccurrences(arr.slice(1), target);
}

console.log(countOccurrences([1, 2, 3, 2, 2], 2));  // 3

Flatten Array #

function flatten(arr) {
  let result = [];

  for (const item of arr) {
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  }

  return result;
}

console.log(flatten([1, [2, [3, 4], 5], 6]));
// [1, 2, 3, 4, 5, 6]

Palindrome Check #

function isPalindrome(str) {
  if (str.length <= 1) {
    return true;
  }

  if (str[0] !== str[str.length - 1]) {
    return false;
  }

  return isPalindrome(str.slice(1, -1));
}

console.log(isPalindrome("racecar"));  // true
console.log(isPalindrome("hello"));    // false

Tree Recursion #

Binary Tree Traversal #

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

// Inorder: Left -> Root -> Right
function inorder(root) {
  if (!root) return;

  inorder(root.left);
  console.log(root.val);
  inorder(root.right);
}

// Preorder: Root -> Left -> Right
function preorder(root) {
  if (!root) return;

  console.log(root.val);
  preorder(root.left);
  preorder(root.right);
}

// Postorder: Left -> Right -> Root
function postorder(root) {
  if (!root) return;

  postorder(root.left);
  postorder(root.right);
  console.log(root.val);
}

Tree Height #

function maxDepth(root) {
  if (!root) {
    return 0;
  }

  const leftDepth = maxDepth(root.left);
  const rightDepth = maxDepth(root.right);

  return Math.max(leftDepth, rightDepth) + 1;
}

Tree Sum #

function sumTree(root) {
  if (!root) {
    return 0;
  }

  return root.val + sumTree(root.left) + sumTree(root.right);
}

Divide and Conquer #

function binarySearch(arr, target, left = 0, right = arr.length - 1) {
  if (left > right) {
    return -1;
  }

  const mid = Math.floor((left + right) / 2);

  if (arr[mid] === target) {
    return mid;
  } else if (arr[mid] > target) {
    return binarySearch(arr, target, left, mid - 1);
  } else {
    return binarySearch(arr, target, mid + 1, right);
  }
}

const arr = [1, 3, 5, 7, 9, 11];
console.log(binarySearch(arr, 7));  // 3

Merge Sort #

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) {
  const result = [];
  let i = 0, j = 0;

  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }

  return result.concat(left.slice(i)).concat(right.slice(j));
}

console.log(mergeSort([5, 2, 8, 1, 9]));
// [1, 2, 5, 8, 9]

Quick Sort #

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);

  return [...quickSort(left), ...middle, ...quickSort(right)];
}

console.log(quickSort([5, 2, 8, 1, 9]));
// [1, 2, 5, 8, 9]

Backtracking #

Generate Permutations #

function permutations(arr) {
  if (arr.length <= 1) {
    return [arr];
  }

  const result = [];

  for (let i = 0; i < arr.length; i++) {
    const current = arr[i];
    const remaining = arr.slice(0, i).concat(arr.slice(i + 1));
    const perms = permutations(remaining);

    for (const perm of perms) {
      result.push([current, ...perm]);
    }
  }

  return result;
}

console.log(permutations([1, 2, 3]));
// [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Generate Subsets #

function subsets(nums) {
  const result = [];

  function backtrack(start, current) {
    result.push([...current]);

    for (let i = start; i < nums.length; i++) {
      current.push(nums[i]);
      backtrack(i + 1, current);
      current.pop();
    }
  }

  backtrack(0, []);
  return result;
}

console.log(subsets([1, 2, 3]));
// [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

N-Queens Problem #

function solveNQueens(n) {
  const result = [];
  const board = Array(n).fill(0).map(() => Array(n).fill('.'));

  function isValid(row, col) {
    // Check column
    for (let i = 0; i < row; i++) {
      if (board[i][col] === 'Q') return false;
    }

    // Check diagonal
    for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
      if (board[i][j] === 'Q') return false;
    }

    // Check anti-diagonal
    for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
      if (board[i][j] === 'Q') return false;
    }

    return true;
  }

  function backtrack(row) {
    if (row === n) {
      result.push(board.map(r => r.join('')));
      return;
    }

    for (let col = 0; col < n; col++) {
      if (isValid(row, col)) {
        board[row][col] = 'Q';
        backtrack(row + 1);
        board[row][col] = '.';
      }
    }
  }

  backtrack(0);
  return result;
}

console.log(solveNQueens(4));

Memoization #

Optimize recursive functions by caching results:

// Without memoization - Exponential time
function fibSlow(n) {
  if (n <= 1) return n;
  return fibSlow(n - 1) + fibSlow(n - 2);
}

// With memoization - Linear time
function fib(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;

  memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
  return memo[n];
}

console.log(fib(50));  // Fast
// console.log(fibSlow(50));  // Very slow!

Memoization Pattern #

function memoize(fn) {
  const cache = {};

  return function(...args) {
    const key = JSON.stringify(args);

    if (key in cache) {
      return cache[key];
    }

    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

// Usage
const fibMemoized = memoize(function(n) {
  if (n <= 1) return n;
  return fibMemoized(n - 1) + fibMemoized(n - 2);
});

Recursion vs Iteration #

Advantages of Recursion #

  • Cleaner code - More readable for some problems
  • Natural fit - Trees, graphs, divide-and-conquer
  • Elegant solutions - Less code for complex problems

Disadvantages of Recursion #

  • Stack overflow - Deep recursion uses stack space
  • Performance - Function call overhead
  • Memory - Each call uses memory

Converting Recursion to Iteration #

Recursive:

function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

Iterative:

function factorial(n) {
  let result = 1;
  for (let i = 1; i <= n; i++) {
    result *= i;
  }
  return result;
}

Common Recursion Patterns #

Linear Recursion #

Single recursive call:

function sum(n) {
  if (n === 0) return 0;
  return n + sum(n - 1);
}

Binary Recursion #

Two recursive calls:

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Multiple Recursion #

More than two recursive calls:

function treeSum(root) {
  if (!root) return 0;
  return root.val + treeSum(root.left) + treeSum(root.right);
}

Tail Recursion #

Recursive call is last operation:

function factorialTail(n, acc = 1) {
  if (n === 0) return acc;
  return factorialTail(n - 1, n * acc);
}

Tips for Writing Recursive Functions #

  1. Identify base case(s) - When to stop?
  2. Define recursive case - How to break down problem?
  3. Make progress - Each call should move toward base case
  4. Test base cases - Verify they return correct values
  5. Trust the recursion - Don’t try to trace all calls
  6. Consider memoization - For overlapping subproblems
  7. Watch stack depth - Be careful with deep recursion

Common Mistakes #

  1. Missing base case - Infinite recursion
  2. Wrong base case - Incorrect results
  3. Not making progress - Never reaches base case
  4. Modifying arguments - Unintended side effects
  5. Stack overflow - Too many recursive calls

Debugging Recursion #

function factorialDebug(n, depth = 0) {
  const indent = ' '.repeat(depth * 2);
  console.log(`${indent}factorial(${n}) called`);

  if (n === 0) {
    console.log(`${indent}Base case: returning 1`);
    return 1;
  }

  const result = n * factorialDebug(n - 1, depth + 1);
  console.log(`${indent}factorial(${n}) = ${result}`);
  return result;
}

factorialDebug(3);

Recursion is a powerful technique for solving complex problems elegantly. Master these patterns and you’ll be able to tackle tree, graph, and divide-and-conquer problems with confidence.