Recursion in Java

  • java
  • recursion
  • algorithms
  • programming

Sources

Recursion in Java

Recursion is a programming technique where a function calls itself to solve a problem. It’s particularly useful for tasks that can be broken down into smaller, similar subproblems.

Understanding Recursion

Recursion occurs when a method calls itself. Every recursive solution has two main components:

  • Base case(s): The condition(s) under which the recursion stops
  • Recursive case(s): The condition(s) under which the method calls itself
Main.java
JAVA
public void recursiveMethod() {
    // Base case
    if (baseCondition) {
        // Do something and return
        return;
    }

    // Recursive case
    // Do something
    recursiveMethod(); // Method calls itself
}
Recursion Visualization
recursiveMethod()
recursiveMethod()
recursiveMethod()
Base case reached

How Recursion Works in Memory

When a method calls itself, each call creates a new activation record (stack frame) on the call stack. This frame contains local variables, parameters, and the return address. As recursion deepens, more frames are added to the stack. When a base case is reached, frames are removed as each call returns.

Simple Recursion Examples

Factorial Calculation

The factorial of a non-negative integer n (denoted as n!) is the product of all positive integers less than or equal to n.

Main.java
JAVA
public static int factorial(int n) {
    // Base case
    if (n == 0 || n == 1) {
        return 1;
    }

    // Recursive case
    return n * factorial(n - 1);
}

Let’s trace the execution of factorial(4):

  1. factorial(4) calls factorial(3) and waits
  2. factorial(3) calls factorial(2) and waits
  3. factorial(2) calls factorial(1) and waits
  4. factorial(1) returns 1 (base case)
  5. factorial(2) computes 2 * 1 = 2 and returns
  6. factorial(3) computes 3 * 2 = 6 and returns
  7. factorial(4) computes 4 * 6 = 24 and returns

Fibonacci Sequence

The Fibonacci sequence is a series of numbers where each number is the sum of the two preceding ones, usually starting with 0 and 1.

Main.java
JAVA
public static int fibonacci(int n) {
    // Base cases
    if (n <= 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }

    // Recursive case
    return fibonacci(n - 1) + fibonacci(n - 2);
}
Fibonacci Recursion Tree for F(4)
Fibonacci recursion tree for F of 4F of 4 branches to F of 3 and F of 2. Each branch continues until reaching the base cases F of 1 and F of 0.F(4) = F(3) + F(2)F(3) = F(2) + F(1)F(2) = F(1) +F(0)F(2) = F(1) +F(0)F(1)= 1F(1)= 1F(0)= 0F(1)= 1F(0)= 0

Recursive Data Structures

Some data structures are naturally recursive, meaning they contain smaller instances of the same structure.

Binary Trees

A binary tree is a tree data structure where each node has at most two children.

Main.java
JAVA
class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;
}

Traversing a binary tree recursively:

Main.java
JAVA
// Inorder traversal: Left -> Root -> Right
public static void inorderTraversal(TreeNode root) {
    if (root == null) {
        return;
    }

    inorderTraversal(root.left);
    System.out.print(root.value + " ");
    inorderTraversal(root.right);
}

Linked Lists

Linked lists can also be processed recursively:

Main.java
JAVA
class ListNode {
    int value;
    ListNode next;

    public ListNode(int value) {
        this.value = value;
        this.next = null;
    }
}

// Print a linked list recursively
public static void printList(ListNode head) {
    if (head == null) {
        return;
    }

    System.out.print(head.value + " ");
    printList(head.next);
}

Common Recursive Algorithms

Tower of Hanoi

The Tower of Hanoi is a classic problem that demonstrates the power of recursion. The goal is to move a stack of disks from one rod to another.

Main.java
JAVA
public static void towerOfHanoi(int n, char source, char auxiliary, char target) {
    if (n == 1) {
        System.out.println("Move disk 1 from " + source + " to " + target);
        return;
    }

    towerOfHanoi(n - 1, source, target, auxiliary);
    System.out.println("Move disk " + n + " from " + source + " to " + target);
    towerOfHanoi(n - 1, auxiliary, source, target);
}

Optimizing with Memoization

Memoization improves recursive performance by storing previously calculated results:

Main.java
JAVA
public class FibonacciMemoization {
    private static Map<Integer, Integer> memo = new HashMap<>();

    public static int fibonacci(int n) {
        if (n <= 0) return 0;
        if (n == 1) return 1;

        // Check if we've already calculated this value
        if (memo.containsKey(n)) {
            return memo.get(n);
        }

        // Calculate and store the result
        int result = fibonacci(n - 1) + fibonacci(n - 2);
        memo.put(n, result);

        return result;
    }
}

Recursion vs. Iteration

Recursion and iteration are two different approaches to solving problems:

  • Recursion: Often simpler and more elegant for problems with recursive structure
  • Iteration: Generally more efficient with memory and speed

Converting Recursion to Iteration

Many recursive algorithms can be converted to iterative ones:

Main.java
JAVA
// Iterative Fibonacci
public static int fibonacciIterative(int n) {
    if (n <= 0) return 0;
    if (n == 1) return 1;

    int fib1 = 0, fib2 = 1, result = 0;

    for (int i = 2; i <= n; i++) {
        result = fib1 + fib2;
        fib1 = fib2;
        fib2 = result;
    }

    return result;
}

Common Pitfalls and Best Practices

Pitfalls

  • Infinite recursion: Forgetting to include a base case
  • Stack overflow: Recursion that is too deep for the available stack space
  • Redundant calculations: Solving the same subproblems multiple times

Best Practices

  • Always include proper base cases
  • Consider the maximum recursion depth and stack limitations
  • Use memoization for overlapping subproblems
  • For performance-critical code, evaluate whether an iterative solution would be more efficient

Conclusion

Recursion is a powerful technique that can lead to elegant solutions for complex problems, especially those with a naturally recursive structure. By understanding when and how to use recursion effectively, you’ll be better equipped to solve a wide range of programming challenges.