JavaScript Recursion: Complete Guide with Examples
Master recursion in JavaScript. Learn recursive functions, base cases, call stack, optimization techniques, and common recursive algorithms.
JavaScript Recursion: Complete Guide with Examples
Recursion is a programming technique where a function calls itself to solve a problem by breaking it down into smaller, similar subproblems. It's a powerful concept that can make complex problems easier to solve and code more elegant.
What is Recursion?
Recursion occurs when a function calls itself directly or indirectly. A recursive function must have two essential components:
- Base case: A condition that stops the recursion
- Recursive case: The function calling itself with modified parameters
// Simple recursive function
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
The Call Stack
Understanding the call stack is crucial for understanding recursion.
// Factorial example
function factorial(n) {
console.log(`factorial(${n}) called`);
// Base case
if (n <= 1) {
console.log(`Returning 1`);
return 1;
}
// Recursive case
const result = n * factorial(n - 1);
console.log(`Returning ${n} * factorial(${n - 1}) = ${result}`);
return result;
}
console.log(factorial(5));
// Call stack visualization:
// factorial(5) -> 5 * factorial(4)
// factorial(4) -> 4 * factorial(3)
// factorial(3) -> 3 * factorial(2)
// factorial(2) -> 2 * factorial(1)
// factorial(1) -> 1 (base case)
// returns 2 * 1 = 2
// returns 3 * 2 = 6
// returns 4 * 6 = 24
// returns 5 * 24 = 120
Stack Overflow
Recursion without a proper base case or with too many recursive calls can cause a stack overflow.
// This will cause a stack overflow
function infiniteRecursion() {
return infiniteRecursion(); // No base case!
}
// This might cause stack overflow for large n
function sumToN(n) {
if (n <= 0) return 0;
return n + sumToN(n - 1);
}
// sumToN(100000); // RangeError: Maximum call stack size exceeded
// Better: Use iteration for simple cases
function sumToNIterative(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
Common Recursive Patterns
1. Linear Recursion
Each function call makes at most one recursive call.
// Array sum
function arraySum(arr, index = 0) {
// Base case
if (index >= arr.length) {
return 0;
}
// Recursive case
return arr[index] + arraySum(arr, index + 1);
}
console.log(arraySum([1, 2, 3, 4, 5])); // 15
// String reversal
function reverseString(str) {
// Base case
if (str.length <= 1) {
return str;
}
// Recursive case
return reverseString(str.slice(1)) + str[0];
}
console.log(reverseString('hello')); // 'olleh'
// Linked list traversal
class ListNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
function printList(node) {
// Base case
if (!node) {
return;
}
// Process current node
console.log(node.value);
// Recursive case
printList(node.next);
}
const list = new ListNode(1, new ListNode(2, new ListNode(3)));
printList(list); // 1, 2, 3
2. Binary Recursion
Each function call makes two recursive calls.
// Fibonacci sequence
function fibonacci(n) {
// Base cases
if (n <= 1) {
return n;
}
// Recursive case (two calls)
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10)); // 55
// Binary tree traversal
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
}
function inorderTraversal(root, result = []) {
if (!root) {
return result;
}
inorderTraversal(root.left, result);
result.push(root.value);
inorderTraversal(root.right, result);
return result;
}
const tree = new TreeNode(2, new TreeNode(1), new TreeNode(3));
console.log(inorderTraversal(tree)); // [1, 2, 3]
// Tree height
function treeHeight(root) {
// Base case
if (!root) {
return 0;
}
// Recursive case
const leftHeight = treeHeight(root.left);
const rightHeight = treeHeight(root.right);
return 1 + Math.max(leftHeight, rightHeight);
}
3. Multiple Recursion
Each function call makes multiple recursive calls.
// Generate all permutations
function permutations(arr) {
// Base case
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 power set (all subsets)
function powerSet(arr) {
// Base case
if (arr.length === 0) {
return [[]];
}
const first = arr[0];
const rest = arr.slice(1);
const subsetsWithoutFirst = powerSet(rest);
const subsetsWithFirst = subsetsWithoutFirst.map((subset) => [
first,
...subset,
]);
return [...subsetsWithoutFirst, ...subsetsWithFirst];
}
console.log(powerSet([1, 2, 3]));
// [[], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]]
Recursive Algorithms
1. Searching Algorithms
// Binary search (recursive)
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
// Base case: not found
if (left > right) {
return -1;
}
const mid = Math.floor((left + right) / 2);
// Base case: found
if (arr[mid] === target) {
return mid;
}
// Recursive cases
if (arr[mid] > target) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}
const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15];
console.log(binarySearch(sortedArray, 7)); // 3
console.log(binarySearch(sortedArray, 6)); // -1
// Depth-first search in a graph
function dfs(graph, node, visited = new Set(), result = []) {
// Mark as visited
visited.add(node);
result.push(node);
// Visit all unvisited neighbors
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
dfs(graph, neighbor, visited, result);
}
}
return result;
}
const graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E'],
};
console.log(dfs(graph, 'A')); // ['A', 'B', 'D', 'E', 'F', 'C']
2. Sorting Algorithms
// Merge sort
function mergeSort(arr) {
// Base case
if (arr.length <= 1) {
return arr;
}
// Divide
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// Conquer (recursive calls)
const sortedLeft = mergeSort(left);
const sortedRight = mergeSort(right);
// Combine
return merge(sortedLeft, sortedRight);
}
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([64, 34, 25, 12, 22, 11, 90]));
// [11, 12, 22, 25, 34, 64, 90]
// Quick sort
function quickSort(arr, low = 0, high = arr.length - 1) {
if (low < high) {
const pivotIndex = partition(arr, low, high);
// Recursively sort elements before and after partition
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
return arr;
}
function partition(arr, low, high) {
const pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
console.log(quickSort([64, 34, 25, 12, 22, 11, 90]));
// [11, 12, 22, 25, 34, 64, 90]
3. Dynamic Programming with Recursion
// Fibonacci with memoization
function fibonacciMemo() {
const memo = new Map();
return function fib(n) {
// Check memo
if (memo.has(n)) {
return memo.get(n);
}
// Base cases
if (n <= 1) {
return n;
}
// Calculate and memoize
const result = fib(n - 1) + fib(n - 2);
memo.set(n, result);
return result;
};
}
const efficientFib = fibonacciMemo();
console.log(efficientFib(50)); // Calculates quickly
// Longest Common Subsequence
function lcs(str1, str2, i = 0, j = 0, memo = new Map()) {
// Create key for memoization
const key = `${i},${j}`;
// Check memo
if (memo.has(key)) {
return memo.get(key);
}
// Base case
if (i === str1.length || j === str2.length) {
return 0;
}
let result;
// If characters match
if (str1[i] === str2[j]) {
result = 1 + lcs(str1, str2, i + 1, j + 1, memo);
} else {
// Try both options
result = Math.max(
lcs(str1, str2, i + 1, j, memo),
lcs(str1, str2, i, j + 1, memo)
);
}
memo.set(key, result);
return result;
}
console.log(lcs('ABCDGH', 'AEDFHR')); // 3 (ADH)
Recursion with Data Structures
1. Tree Operations
// Binary search tree operations
class BSTNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BST {
constructor() {
this.root = null;
}
insert(value) {
this.root = this.insertNode(this.root, value);
}
insertNode(node, value) {
// Base case
if (!node) {
return new BSTNode(value);
}
// Recursive cases
if (value < node.value) {
node.left = this.insertNode(node.left, value);
} else if (value > node.value) {
node.right = this.insertNode(node.right, value);
}
return node;
}
search(value) {
return this.searchNode(this.root, value);
}
searchNode(node, value) {
// Base cases
if (!node) {
return false;
}
if (node.value === value) {
return true;
}
// Recursive cases
if (value < node.value) {
return this.searchNode(node.left, value);
} else {
return this.searchNode(node.right, value);
}
}
findMin(node = this.root) {
// Base case
if (!node.left) {
return node.value;
}
// Recursive case
return this.findMin(node.left);
}
delete(value) {
this.root = this.deleteNode(this.root, value);
}
deleteNode(node, value) {
// Base case
if (!node) {
return null;
}
// Find the node to delete
if (value < node.value) {
node.left = this.deleteNode(node.left, value);
} else if (value > node.value) {
node.right = this.deleteNode(node.right, value);
} else {
// Node found - three cases
// Case 1: No children
if (!node.left && !node.right) {
return null;
}
// Case 2: One child
if (!node.left) {
return node.right;
}
if (!node.right) {
return node.left;
}
// Case 3: Two children
const minRight = this.findMin(node.right);
node.value = minRight;
node.right = this.deleteNode(node.right, minRight);
}
return node;
}
}
// Tree serialization
function serialize(root) {
if (!root) {
return 'null';
}
return `${root.value},${serialize(root.left)},${serialize(root.right)}`;
}
function deserialize(data) {
const values = data.split(',');
let index = 0;
function buildTree() {
if (index >= values.length || values[index] === 'null') {
index++;
return null;
}
const node = new TreeNode(parseInt(values[index++]));
node.left = buildTree();
node.right = buildTree();
return node;
}
return buildTree();
}
2. Graph Algorithms
// Detect cycle in directed graph
function hasCycle(graph) {
const visited = new Set();
const recursionStack = new Set();
function dfsHasCycle(node) {
visited.add(node);
recursionStack.add(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
if (dfsHasCycle(neighbor)) {
return true;
}
} else if (recursionStack.has(neighbor)) {
return true;
}
}
recursionStack.delete(node);
return false;
}
for (const node in graph) {
if (!visited.has(node)) {
if (dfsHasCycle(node)) {
return true;
}
}
}
return false;
}
// Topological sort
function topologicalSort(graph) {
const visited = new Set();
const stack = [];
function dfs(node) {
visited.add(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
dfs(neighbor);
}
}
stack.push(node);
}
for (const node in graph) {
if (!visited.has(node)) {
dfs(node);
}
}
return stack.reverse();
}
const dag = {
A: ['C'],
B: ['C', 'D'],
C: ['E'],
D: ['F'],
E: ['F'],
F: [],
};
console.log(topologicalSort(dag)); // ['B', 'D', 'A', 'C', 'E', 'F']
Optimization Techniques
1. Tail Recursion
Tail recursion occurs when the recursive call is the last operation in the function.
// Not tail recursive
function factorialRegular(n) {
if (n <= 1) return 1;
return n * factorialRegular(n - 1); // Multiplication after recursive call
}
// Tail recursive
function factorialTail(n, accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // Recursive call is last
}
// Tail recursive sum
function sumTail(arr, index = 0, accumulator = 0) {
if (index >= arr.length) {
return accumulator;
}
return sumTail(arr, index + 1, accumulator + arr[index]);
}
// Note: JavaScript doesn't optimize tail recursion by default
// But the pattern is still useful for understanding
2. Memoization
Cache results of expensive recursive calls.
// Generic memoization function
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Memoized recursive functions
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
// Grid paths problem
const gridPaths = memoize((m, n) => {
if (m === 1 || n === 1) return 1;
return gridPaths(m - 1, n) + gridPaths(m, n - 1);
});
console.log(gridPaths(18, 18)); // Calculates quickly with memoization
3. Converting to Iteration
Sometimes converting recursion to iteration improves performance.
// Recursive approach
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// Iterative approach
function fibIterative(n) {
if (n <= 1) return n;
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
const next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
// Stack-based iteration (simulating recursion)
function inorderIterative(root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length > 0) {
// Go to leftmost node
while (current) {
stack.push(current);
current = current.left;
}
// Current is null here
current = stack.pop();
result.push(current.value);
// Visit right subtree
current = current.right;
}
return result;
}
Common Pitfalls and Solutions
1. Missing Base Case
// Wrong: No base case
function badRecursion(n) {
return badRecursion(n - 1); // Infinite recursion!
}
// Correct: Always have a base case
function goodRecursion(n) {
if (n <= 0) return; // Base case
console.log(n);
goodRecursion(n - 1);
}
2. Incorrect Base Case
// Wrong: Base case doesn't cover all scenarios
function sumArray(arr, index = 0) {
if (index === arr.length - 1) {
// Misses empty array
return arr[index];
}
return arr[index] + sumArray(arr, index + 1);
}
// Correct: Handle all edge cases
function sumArrayCorrect(arr, index = 0) {
if (index >= arr.length) {
// Handles empty array
return 0;
}
return arr[index] + sumArrayCorrect(arr, index + 1);
}
3. Stack Overflow
// Use iteration for simple linear recursion
function factorial(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// Or increase stack size (Node.js)
// node --stack-size=10000 script.js
// Or use trampolining for tail recursion
function trampoline(fn) {
return function (...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}
const factorialTrampoline = trampoline(function factorial(n, acc = 1) {
if (n <= 1) return acc;
return () => factorial(n - 1, n * acc);
});
Best Practices
1. Choose the Right Approach
// Use recursion when:
// - Problem has recursive structure (trees, nested data)
// - Divide-and-conquer algorithms
// - Backtracking problems
// Use iteration when:
// - Simple linear processing
// - Performance is critical
// - Stack depth might be an issue
// Example: Tree problems are naturally recursive
function maxDepth(root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// Example: Simple loops are better iterative
function findMax(arr) {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
}
return max;
}
2. Clear Base Cases
// Document base cases clearly
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
// Base case 1: Element not found
if (left > right) {
return -1;
}
const mid = Math.floor((left + right) / 2);
// Base case 2: Element found
if (arr[mid] === target) {
return mid;
}
// Recursive cases with clear conditions
if (target < arr[mid]) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}
3. Helper Functions
// Public interface with clean API
function reverseLinkedList(head) {
// Helper function handles the recursion
function reverseHelper(current, previous = null) {
// Base case
if (!current) {
return previous;
}
// Save next node
const next = current.next;
// Reverse pointer
current.next = previous;
// Recursive call
return reverseHelper(next, current);
}
return reverseHelper(head);
}
// Clean parameters using closure
function permutations(arr) {
const result = [];
function generate(current, remaining) {
if (remaining.length === 0) {
result.push(current);
return;
}
for (let i = 0; i < remaining.length; i++) {
generate(
[...current, remaining[i]],
[...remaining.slice(0, i), ...remaining.slice(i + 1)]
);
}
}
generate([], arr);
return result;
}
Conclusion
Recursion is a powerful programming technique that:
- Simplifies complex problems by breaking them into smaller subproblems
- Naturally handles recursive data structures like trees and graphs
- Enables elegant solutions for algorithms like divide-and-conquer
- Requires careful consideration of base cases and stack limitations
Key takeaways:
- Always define clear base cases
- Consider memoization for overlapping subproblems
- Be aware of stack limitations
- Sometimes iteration is more appropriate
- Practice with tree and graph problems to master recursion
Understanding recursion is essential for:
- Algorithm interviews
- Working with hierarchical data
- Implementing sophisticated algorithms
- Functional programming paradigms
Master recursion to unlock powerful problem-solving techniques and write more elegant code!