Data Structure and Algorithm
Algorithms
Recursion

Introduction to Recursion in JavaScript

Recursion is a powerful concept in programming that allows us to solve complex problems more efficiently. In JavaScript, recursion can simplify tasks that would otherwise require lengthy and complicated loops. Understanding recursion is essential for tackling advanced topics like dynamic programming and data structures.

What is Recursion?

Recursion is a process where a function calls itself directly or indirectly. A recursive function keeps breaking down a problem into smaller instances of the same problem until it reaches a base condition, which stops the recursion. This approach can be highly effective in simplifying complex problems.

Recursion vs. Loops

While loops iterate in a single direction, moving from one step to the next until a condition is met, recursion works differently. Recursion moves in two directions:

  1. Forward - As the function calls itself, it progresses toward the base condition.
  2. Backward - After reaching the base condition, the function starts returning, solving parts of the problem as it unwinds.

This bidirectional nature of recursion allows it to handle complex tasks that loops might struggle with.

Structure of a Recursive Function

A typical recursive function in JavaScript has three main components:

  1. Base Condition: This is the exit condition that stops the recursion. Without it, the function would call itself indefinitely, leading to a stack overflow.
  2. Logic: The core logic that performs the main task, usually involving calculations or processing.
  3. Recursive Call: The function calls itself with a modified argument, moving closer to the base condition.

Here’s a general template for a recursive function:

function recursive() {
    // Base exit condition
    if (/* base condition */) {
        return;
    }
 
    // Logic
    // Perform the necessary operations here
 
    // Recursive call
    recursive();
}

Example 1: Simple Recursion

Let’s start with a simple example that prints all elements of an array using recursion:

const array = [10, 22, 30, 4, 7];
 
function print(array, startIndex) {
    // Base exit condition
    if (startIndex >= array.length) {
        return;
    }
 
    // Logic
    console.log(array[startIndex]);
 
    // Recursive call
    print(array, startIndex + 1);
}
 
print(array, 0);

In this example, the function print recursively prints each element in the array, moving from the first to the last index.

Example 2: Reverse Printing Using Recursion

Now, let's modify the above function to print the array elements in reverse order:

function revPrint(array, startIndex) {
    // Base exit condition
    if (startIndex >= array.length) {
        return;
    }
 
    // Recursive call
    revPrint(array, startIndex + 1);
 
    // Logic
    console.log(array[startIndex]);
}
 
revPrint(array, 0);

Here, the function first makes the recursive call and then prints the current element. This approach effectively reverses the order of printing.

Example 3: Factorial Calculation

Recursion is often used to solve mathematical problems like calculating the factorial of a number:

function factorial(num) {
    // Base case
    if (num === 0 || num === 1) {
        return 1;
    }
    return num * factorial(num - 1);
}
 
console.log(factorial(4)); // Output: 24

In this example, the factorial of n is calculated by multiplying n with the factorial of n-1, and the recursion continues until it reaches the base case where num is 0 or 1.

Example 4: Summing Array Elements

Here's a recursive function to sum all the elements in an array:

function sumArrayElement(array, startIndex) {
    // Base case
    if (startIndex >= array.length) {
        return 0;
    }
    return array[startIndex] + sumArrayElement(array, startIndex + 1);
}
 
console.log(sumArrayElement(array, 0)); // Output: 73

In this example, the function recursively adds each element of the array, moving from the first to the last element.

Why Recursion is Different from Loops

  • Bidirectional Execution: Unlike loops, recursion processes the function in both forward and backward directions, making it more flexible and powerful in certain scenarios.
  • Simplification: Recursive solutions often simplify complex problems and reduce lengthy loops into concise functions, enhancing code readability and maintainability.

Types of Recursion

Recursion can be categorized into several types:

  • Tail Recursion: The recursive call is the last operation in the function.
  • Head Recursion: The recursive call is the first operation, followed by other operations.
  • Tree Recursion: The function makes multiple recursive calls.
  • Indirect Recursion: A function calls another function, which in turn calls the first function.

How Recursion Works

Recursion works by leveraging the call stack, a data structure that stores information about active functions. Each time a recursive function is called, a new frame (or instance) is added to the stack. When the base condition is met, these frames are popped off the stack as the function unwinds.

Generalization

Recursion can handle complex problems by breaking them into simpler sub-problems. This divide-and-conquer strategy is effective for tasks like sorting, searching, and traversing hierarchical structures like trees.

Recursion and the Stack

Each recursive call occupies stack space. If a recursion lacks a proper base case or involves too many calls, it may lead to a stack overflow error. Understanding stack operations is crucial to write efficient recursive functions.

Recurrence Relation and Time Complexity

Recurrence relations are mathematical equations that describe the time complexity of recursive algorithms. For example, the recurrence relation for calculating factorial is:

[ T(n) = T(n-1) + O(1) ]

This implies a linear time complexity, or ( O(n) ), for this algorithm. Similarly, different problems result in different recurrence relations, which can be analyzed to determine the algorithm's efficiency.

Static and Global Variables in Recursion

Static Variables

Static variables retain their values across recursive calls. In JavaScript, closures or external variables can mimic this behavior.

Global Variables

Global variables can also hold states across recursive calls, but their use is discouraged due to potential side effects and reduced readability.

let counter = 0;
 
function staticExample() {
    if (counter >= 5) {
        return;
    }
    console.log(counter);
    counter++;
    staticExample();
}
 
staticExample();

Types of Recursion

Tail Recursion

In tail recursion, the recursive call is the last operation in the function. It can be optimized by some compilers to avoid stacking frames.

function tailRecSum(n, accumulator = 0) {
    if (n === 0) return accumulator;
    return tailRecSum(n - 1, accumulator + n);
}
 
console.log(tailRecSum(5)); // Output: 15

Head Recursion

In head recursion, the recursive call occurs first, followed by other operations.

function headRecSum(n) {
    if (n === 0) return 0;
    return n + headRecSum(n - 1);
}
 
console.log(headRecSum(5)); // Output: 15

Tree Recursion

In tree recursion, a function makes multiple recursive calls.

function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}
 
console.log(fib(5)); // Output: 5

Indirect Recursion

In indirect recursion, one function calls another, which in turn calls the first function.

function A(x) {
    if (x > 0) B(x - 1);
}
 
function B(x) {
    if (x > 0) A(x - 2);
}
 
A(3);

Nested Recursion

In nested recursion, the recursive call itself contains another recursive call.

function nested(n) {
    if (n > 100) return n - 10;
    return nested(nested(n + 11));
}
 
console.log(nested(95)); // Output: 91

Conclusion

Recursion is not just a programming technique; it’s a mindset. By breaking down complex problems into simpler sub-problems, recursion enables more efficient and elegant solutions. Understanding its mechanics, types, and applications is vital for any developer looking to master advanced algorithms and data structures.