Recursion in Java

April 19, 2025

javarecursionalgorithmsprogramming

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:

  1. Base case(s): The condition(s) under which the recursion stops
  2. Recursive case(s): The condition(s) under which the method calls itself
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.

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.

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)

F(4) = F(3) + F(2)
F(3) = F(2) + F(1)
F(2) = F(1) + F(0)
F(1) = 1
F(0) = 0
F(1) = 1
F(2) = F(1) + F(0)
F(1) = 1
F(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.

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;
}

Traversing a binary tree recursively:

// 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:

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.

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:

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:

// 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

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

Best Practices

  1. Always include proper base cases
  2. Consider the maximum recursion depth and stack limitations
  3. Use memoization for overlapping subproblems
  4. 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.