Skip to content
beginnerPhase 9 · Java Foundations

Recursion in Java

Implement recursive solutions in Java with proper base cases and stack understanding.

1h
4 problems
Topic Progress0%

How Recursion Works

What is Recursion?

A method that calls itself. Each call works on a smaller problem until reaching a base case.

Anatomy of Recursion

public class Recursion {
    // Base case: stops recursion
    // Recursive case: calls itself with smaller input
    
    // Example: Factorial
    public static long factorial(int n) {
        // Base case
        if (n <= 1) {
            return 1;
        }
        // Recursive case
        return n * factorial(n - 1);
    }
    
    public static void main(String[] args) {
        System.out.println(factorial(5));  // 120
        System.out.println(factorial(10)); // 3628800
    }
}

Recursion Trace

factorial(5)
  5 * factorial(4)
    4 * factorial(3)
      3 * factorial(2)
        2 * factorial(1)
          return 1
        return 2 * 1 = 2
      return 3 * 2 = 6
    return 4 * 6 = 24
  return 5 * 24 = 120

Two Essential Parts

  1. Base case: Condition to stop (no more recursive calls)
  2. Recursive case: Calls itself with modified input toward base case
// Missing base case = infinite recursion = StackOverflowError!
public static void infinite() {
    infinite();  // NO BASE CASE!
}

// Always ensure progress toward base case
public static void countdown(int n) {
    if (n <= 0) {  // base case
        System.out.println("Done!");
        return;
    }
    System.out.println(n);
    countdown(n - 1);  // progress toward base case
}

Stack Frames and Recursion

How Recursion Uses the Stack

Each method call creates a new stack frame containing:

  • Local variables
  • Parameters
  • Return address
public class StackFrames {
    public static int sum(int n) {
        if (n == 0) return 0;
        return n + sum(n - 1);
    }
    
    public static void main(String[] args) {
        // Each call adds a frame to the stack:
        // sum(4) → frame 1
        //   sum(3) → frame 2
        //     sum(2) → frame 3
        //       sum(1) → frame 4
        //         sum(0) → frame 5 (base case)
        //       return 0
        //     return 1 + 0 = 1
        //   return 2 + 1 = 3
        // return 3 + 3 = 6
        // return 4 + 6 = 10
        
        System.out.println(sum(4));  // 10
    }
}

Stack Overflow

// Too many recursive calls cause StackOverflowError
public class StackOverflow {
    public static void recurse(int n) {
        if (n == 0) return;
        recurse(n - 1);
    }
    
    public static void main(String[] args) {
        try {
            recurse(100000);  // too deep!
        } catch (StackOverflowError e) {
            System.out.println("Stack overflow!");
        }
    }
}

// Stack size varies:
// - Usually 512KB to 1MB
// - Each frame ~100-1000 bytes
// - Max depth ~10,000-50,000 calls

Visualizing Stack Frames

┌─────────────────────┐
│ main()              │  Stack
├─────────────────────┤
│ factorial(5)        │  ← n=5
├─────────────────────┤
│ factorial(4)        │  ← n=4
├─────────────────────┤
│ factorial(3)        │  ← n=3
├─────────────────────┤
│ factorial(2)        │  ← n=2
├─────────────────────┤
│ factorial(1)        │  ← n=1 (base case)
└─────────────────────┘

Tail Recursion

// Regular recursion: operation AFTER recursive call
public static long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // multiplication after call
}

// Tail recursion: operation BEFORE recursive call
public static long factorialTail(int n, long acc) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);  // multiplication before call
}

// Java does NOT optimize tail recursion!
// But it's still a useful pattern

// Usage:
// factorialTail(5, 1) → factorialTail(4, 5) → factorialTail(3, 20) → ...
// Same result, but no pending operations on stack

Writing Base Cases

Common Base Case Patterns

// Pattern 1: Simple boundary
public static int sum(int[] arr, int index) {
    if (index >= arr.length) return 0;  // base case
    return arr[index] + sum(arr, index + 1);
}

// Pattern 2: Value check
public static int power(int base, int exp) {
    if (exp == 0) return 1;  // base case
    if (exp < 0) return 1 / power(base, -exp);  // handle negatives
    return base * power(base, exp - 1);
}

// Pattern 3: String length
public static boolean isPalindrome(String s) {
    if (s.length() <= 1) return true;  // base case
    if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
    return isPalindrome(s.substring(1, s.length() - 1));
}

// Pattern 4: Two pointers
public static boolean isPalindrome(char[] arr, int left, int right) {
    if (left >= right) return true;  // base case
    if (arr[left] != arr[right]) return false;
    return isPalindrome(arr, left + 1, right - 1);
}

Multi-way Base Cases

// Fibonacci
public static int fib(int n) {
    if (n <= 0) return 0;  // base case 1
    if (n == 1) return 1;  // base case 2
    return fib(n - 1) + fib(n - 2);
}

// Tower of Hanoi
public static void hanoi(int n, char from, char to, char aux) {
    if (n == 1) {  // base case
        System.out.println("Move disk 1 from " + from + " to " + to);
        return;
    }
    hanoi(n - 1, from, aux, to);
    System.out.println("Move disk " + n + " from " + from + " to " + to);
    hanoi(n - 1, aux, to, from);
}

Ensuring Progress

// BAD: no progress toward base case
public static void badRecursion(int n) {
    if (n == 0) return;
    badRecursion(n);  // same n! infinite loop
}

// GOOD: progress toward base case
public static void goodRecursion(int n) {
    if (n == 0) return;
    goodRecursion(n - 1);  // n decreases!
}

// Check: does each recursive call move closer to base case?
// If n decreases: progress ✓
// If n stays same: infinite recursion ✗

Common Recursive Patterns

Pattern 1: Linear Recursion

// Factorial
public static long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// Sum of array
public static int sum(int[] arr, int index) {
    if (index >= arr.length) return 0;
    return arr[index] + sum(arr, index + 1);
}

// Reverse string
public static String reverse(String s) {
    if (s.length() <= 1) return s;
    return reverse(s.substring(1)) + s.charAt(0);
}

Pattern 2: Binary Recursion

// Fibonacci (two recursive calls)
public static int fib(int n) {
    if (n <= 0) return 0;
    if (n == 1) return 1;
    return fib(n - 1) + fib(n - 2);
}

// Power (efficient)
public static double power(double base, int exp) {
    if (exp == 0) return 1;
    if (exp % 2 == 0) {
        double half = power(base, exp / 2);
        return half * half;
    }
    return base * power(base, exp - 1);
}

Pattern 3: Divide and Conquer

// Merge Sort
public static void mergeSort(int[] arr, int left, int right) {
    if (left >= right) return;  // base case
    
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid);      // sort left half
    mergeSort(arr, mid + 1, right); // sort right half
    merge(arr, left, mid, right);   // merge halves
}

// Binary Search
public static int binarySearch(int[] arr, int target, int left, int right) {
    if (left > right) return -1;  // base case
    
    int mid = left + (right - left) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] < target) return binarySearch(arr, target, mid + 1, right);
    return binarySearch(arr, target, left, mid - 1);
}

Pattern 4: Backtracking

// Generate all subsets
public static void subsets(int[] nums, int index, List<Integer> current, List<List<Integer>> result) {
    if (index >= nums.length) {
        result.add(new ArrayList<>(current));  // base case
        return;
    }
    
    // Don't include nums[index]
    subsets(nums, index + 1, current, result);
    
    // Include nums[index]
    current.add(nums[index]);
    subsets(nums, index + 1, current, result);
    current.remove(current.size() - 1);  // backtrack
}

// Permutations
public static void permute(int[] nums, int start, List<List<Integer>> result) {
    if (start == nums.length) {
        result.add(Arrays.stream(nums).boxed().collect(Collectors.toList()));
        return;
    }
    
    for (int i = start; i < nums.length; i++) {
        swap(nums, start, i);
        permute(nums, start + 1, result);
        swap(nums, start, i);  // backtrack
    }
}

Pattern 5: Tree Recursion

// Tree node
class TreeNode {
    int val;
    TreeNode left, right;
}

// Tree traversals
public void inorder(TreeNode node) {
    if (node == null) return;  // base case
    inorder(node.left);
    System.out.print(node.val + " ");
    inorder(node.right);
}

// Tree height
public int height(TreeNode node) {
    if (node == null) return 0;
    return 1 + Math.max(height(node.left), height(node.right));
}

// Check if tree is balanced
public boolean isBalanced(TreeNode node) {
    if (node == null) return true;
    int leftHeight = height(node.left);
    int rightHeight = height(node.right);
    return Math.abs(leftHeight - rightHeight) <= 1
        && isBalanced(node.left)
        && isBalanced(node.right);
}

Practice Problems

0/4solved
Fibonacci Number
Recursion

Calculate the nth Fibonacci number recursively.

Example:

Input: n = 5

Output: 5

Fibonacci sequence: 0, 1, 1, 2, 3, 5. fib(5) = 5.

Optimal Solution — O(2^n) time, O(n) (stack depth) space

Simple recursion with base cases

class Solution {
    public int fib(int n) {
        if (n <= 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }
}

Edge Cases:

  • n=0
  • n=1
  • Large n (use memoization)
Power of Number
Recursion

Calculate x raised to power n recursively.

Example:

Input: x = 2, n = 10

Output: 1024

2^10 = 1024

Optimal Solution — O(log n) time, O(log n) space

Efficient recursion with halving

class Solution {
    public double myPow(double x, int n) {
        if (n == 0) return 1;
        if (n < 0) {
            x = 1 / x;
            n = -n;
        }
        if (n % 2 == 0) {
            double half = myPow(x, n / 2);
            return half * half;
        }
        return x * myPow(x, n - 1);
    }
}

Edge Cases:

  • Negative exponent
  • x=0
  • x=1
Reverse String Recursively
Recursion

Reverse a string using recursion.

Example:

Input: s = "hello"

Output: olleh

Reverse each character recursively.

Optimal Solution — O(n²) time, O(n) space

Recursive string reversal

class Solution {
    public String reverseString(String s) {
        if (s.length() <= 1) return s;
        return reverseString(s.substring(1)) + s.charAt(0);
    }
}

Edge Cases:

  • Empty string
  • Single character
Sum of Digits
Recursion

Find sum of digits of a number recursively.

Example:

Input: n = 12345

Output: 15

1+2+3+4+5 = 15

Optimal Solution — O(d) where d is number of digits time, O(d) space

Recursive digit extraction

class Solution {
    public int sumOfDigits(int n) {
        if (n < 10) return n;
        return (n % 10) + sumOfDigits(n / 10);
    }
}

Edge Cases:

  • Single digit
  • Zero

Quiz

1. What are the two essential parts of a recursive method?

Question 1 options

2. What happens if a recursive method has no base case?

Question 2 options

3. Does Java optimize tail recursion?

Question 3 options

4. What is the space complexity of a recursive function with depth n?

Question 4 options

Flashcards

Question

What is the base case in recursion?

Answer

The condition that stops recursion. Without it, the method calls itself infinitely causing StackOverflowError.

Question

What is tail recursion?

Answer

When the recursive call is the last operation. Java does NOT optimize tail recursion, unlike some other languages.

Question

What causes StackOverflowError in recursion?

Answer

Too many recursive calls (no base case or too deep). Each call uses ~100-1000 bytes of stack space.

Question

When should you use iteration over recursion?

Answer

When recursion depth is too large, or for simple loops. Iteration avoids stack overflow and is often faster.

Question

What is Recursion in Java?

Answer

Recursion in Java is a key concept in Java programming.

Revision Notes

Key Takeaways

  • 1.Always have a base case to stop recursion
  • 2.Each recursive call must progress toward the base case
  • 3.Recursion uses O(n) stack space
  • 4.Java does NOT optimize tail recursion
  • 5.Use iteration when recursion depth is too large

Interview Tips

  • Start with base case, then recursive case
  • Trace through recursive calls to verify correctness
  • Consider time/space tradeoffs vs iteration
  • Practice common patterns: factorial, fibonacci, binary search, trees

Cheat Sheet

Recursion Cheat Sheet

Structure:

void recurse(params) {
    if (baseCase) return;
    // work
    recurse(smallerInput);  // progress toward base
}

Essential Parts:

  1. Base case: stops recursion
  2. Recursive case: calls itself
  3. Progress: moves toward base case

Common Patterns:

  • Factorial: n * factorial(n-1)
  • Fibonacci: fib(n-1) + fib(n-2)
  • Binary Search: O(log n)
  • Tree traversal: O(n)

Stack:

  • Each call adds a frame
  • Max depth ~10,000-50,000
  • Java does NOT optimize tail recursion

Time Complexities:

  • Factorial: O(n)
  • Fibonacci (naive): O(2^n)
  • Binary Search: O(log n)