Skip to content
beginnerPhase 1 · Foundation

Recursion

Understand recursive thinking, call stacks, and the foundation for trees, graphs, and DP.

1h 30m
5 problems
Topic Progress0%

What is Recursion

Recursion is a technique where a function calls itself to solve a smaller version of the same problem.

Real-World Analogy

Imagine you're in a line at a coffee shop and want to know your position. You ask the person ahead: "What position are you?" They ask the person ahead of them. This continues until someone at the front says "I'm first!" Then the answer propagates back: "I'm second," "I'm third," until you learn your position.

That's recursion — solving a problem by breaking it into smaller instances of itself.

How It Works in Memory

public int factorial(int n) {
    if (n <= 1) return 1;        // Base case
    return n * factorial(n - 1);  // Recursive case
}

When factorial(4) is called:

Call Stack Growth:

factorial(4)     →  n=4, waits for factorial(3)
  factorial(3)   →  n=3, waits for factorial(2)
    factorial(2) →  n=2, waits for factorial(1)
      factorial(1) → n=1, returns 1  ← Base case hit!
    returns 2 * 1 = 2
  returns 3 * 2 = 6
returns 4 * 6 = 24

Two Essential Components

  1. Base Case: The condition that stops the recursion. Without it, you get a StackOverflowError.
  2. Recursive Case: The part where the function calls itself with a smaller input.
public int sum(int n) {
    if (n <= 0) return 0;     // Base case: stops recursion
    return n + sum(n - 1);    // Recursive case: smaller problem
}

Why Recursion Matters

Many problems are naturally recursive:

  • Trees: Every subtree is a smaller tree
  • Graphs: Visiting neighbors is exploring a smaller graph
  • Divide and Conquer: Splitting a problem in half
  • Dynamic Programming: Breaking into subproblems

Without recursion, you'd need explicit stacks and complex loop logic for these problems.

Anatomy of a Recursive Function

Every recursive function follows this template:

ReturnType solve(problem) {
    // Step 1: Base case
    if (problem is small enough) {
        return direct solution;
    }
    
    // Step 2: Recursive case
    smallerProblem = reduce(problem);
    recursiveResult = solve(smallerProblem);
    
    // Step 3: Combine results
    return combine(current, recursiveResult);
}

Example: Power Function

public double myPow(double x, int n) {
    // Base case
    if (n == 0) return 1.0;
    
    // Handle negative exponents
    if (n < 0) {
        x = 1 / x;
        n = -n;
    }
    
    // Recursive case: x^n = x * x^(n-1)
    return x * myPow(x, n - 1);
}
// Time: O(n), Space: O(n) for call stack

Example: Sum of Digits

public int digitSum(int n) {
    // Base case: single digit
    if (n < 10) return n;
    
    // Recursive case: last digit + sum of rest
    return (n % 10) + digitSum(n / 10);
}
// digitSum(1234) → 4 + digitSum(123) → 4 + 3 + digitSum(12) → 4 + 3 + 2 + digitSum(1) → 4 + 3 + 2 + 1 = 10

Example: Reverse String

public String reverse(String s) {
    if (s.length() <= 1) return s;
    return reverse(s.substring(1)) + s.charAt(0);
}
// reverse("hello") → reverse("ello") + "h" → "olle" + "h" → "olleh"

The Call Stack Visualization

reverse("hello")
  reverse("ello")
    reverse("llo")
      reverse("lo")
        reverse("o")
          returns "o"
        returns "o" + "l" = "ol"
      returns "ol" + "l" = "oll"
    returns "oll" + "e" = "olle"
  returns "olle" + "h" = "olleh"

Key Insight

The base case is the most important part. Always ask: "What is the smallest version of this problem that I can solve directly?"

Recursion Patterns

Pattern 1: Linear Recursion

One recursive call per function invocation.

// Factorial: f(n) = n * f(n-1)
public int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
// Call tree: linear chain
// factorial(5) → 5 * factorial(4) → 5 * 4 * factorial(3) → ...

Use when: Problem reduces by a constant amount each step.

Pattern 2: Binary Recursion

Two recursive calls per function invocation.

// Fibonacci: f(n) = f(n-1) + f(n-2)
public int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}
// Call tree: binary tree
//           fib(5)
//          /       \
//      fib(4)     fib(3)
//      /    \\     /    \
//  fib(3) fib(2) fib(2) fib(1)

Warning: Without memoization, this is O(2^n). See DP topic for optimization.

Pattern 3: Tail Recursion

The recursive call is the last operation.

// Tail-recursive factorial
public int factorialHelper(int n, int accumulator) {
    if (n <= 1) return accumulator;
    return factorialHelper(n - 1, n * accumulator);  // Last operation
}
public int factorial(int n) {
    return factorialHelper(n, 1);
}

Advantage: Can be optimized to iteration by the compiler (Java doesn't do this, but it's good practice).

Pattern 4: Tree Recursion

Recursive calls mirror tree structure.

// Traverse a binary tree
public void traverse(TreeNode root) {
    if (root == null) return;
    
    System.out.println(root.val);  // Process current
    traverse(root.left);            // Recurse left
    traverse(root.right);           // Recurse right
}

Use when: Problem has hierarchical structure (trees, graphs, partitions).

Pattern Selection Guide

Pattern Structure Example Complexity
Linear Chain Factorial, Sum O(n) time, O(n) space
Binary Tree Fibonacci, Merge Sort O(2^n) without memo
Tail Chain with accumulator Factorial, Reverse O(n) time, O(1) if optimized
Tree Tree Tree traversal, Parse O(n) time, O(h) space

Analyzing Recursive Complexity

Time Complexity

Count the number of recursive calls and work done per call.

Example: Factorial

public int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // One call, O(1) work
}
// Total calls: n
// Work per call: O(1)
// Time: O(n)

Example: Fibonacci (Naive)

public int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);  // Two calls
}
// Total calls: O(2^n) — exponential!
// Why? Each call branches into two, creating a binary tree of calls

Space Complexity

Space = maximum depth of recursion stack.

factorial(5) → depth 5
  factorial(4) → depth 4
    factorial(3) → depth 3
      factorial(2) → depth 2
        factorial(1) → depth 1 (base case)

Maximum depth = n → Space = O(n)

Recurrence Relations

For complex recursion, use recurrences:

Recurrence Solution Example
T(n) = T(n-1) + O(1) O(n) Factorial
T(n) = T(n-1) + O(n) O(n²) Selection sort
T(n) = 2T(n/2) + O(n) O(n log n) Merge sort
T(n) = 2T(n-1) + O(1) O(2^n) Naive Fibonacci
T(n) = T(n/2) + O(1) O(log n) Binary search

Mastering the Recursion Tree

Draw the tree to count calls:

fib(5)
├── fib(4)
│   ├── fib(3)
│   │   ├── fib(2)
│   │   │   ├── fib(1) = 1
│   │   │   └── fib(0) = 0
│   │   └── fib(1) = 1
│   └── fib(2)
│       ├── fib(1) = 1
│       └── fib(0) = 0
└── fib(3)
    ├── fib(2)
    │   ├── fib(1) = 1
    │   └── fib(0) = 0
    └── fib(1) = 1

Total nodes: 15 = O(2^5)

Common Mistakes

  1. Missing base case → StackOverflowError
  2. Wrong base case → Incorrect results
  3. Not making progress → Infinite recursion
  4. Redundant computation → Use memoization (see DP)

Recursion vs Iteration

Converting Recursion to Iteration

Every recursive solution can be converted to iteration using an explicit stack.

Recursive:

public void dfs(TreeNode root) {
    if (root == null) return;
    System.out.println(root.val);
    dfs(root.left);
    dfs(root.right);
}

Iterative (using explicit stack):

public void dfs(TreeNode root) {
    if (root == null) return;
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.println(node.val);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
}

When to Use Which

Aspect Recursion Iteration
Code clarity Cleaner for tree/graph Can be complex
Stack space O(depth) implicit O(1) if no stack
Risk StackOverflowError Infinite loop
Debugging Harder Easier
Performance Function call overhead Generally faster

When Recursion is Better

  • Trees: Natural recursive structure
  • Graphs: DFS is naturally recursive
  • Divide and Conquer: Splitting problems
  • Backtracking: Exploring all possibilities

When Iteration is Better

  • Simple loops: For, while, do-while
  • Tail recursion: Can be optimized
  • Deep recursion: Risk of stack overflow
  • Performance critical: Avoid function call overhead

Java Tail Call Optimization

Java does NOT optimize tail recursion. For deep recursion, convert to iteration:

// Bad: O(n) stack space
public int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// Good: O(1) space
public int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

Practice Problems

0/5solved
Factorial
Linear Recursion

Given an integer n, return n! (n factorial). n! = n * (n-1) * ... * 1. 0! = 1.

Example:

Input: n = 5

Output: 120

5! = 5 × 4 × 3 × 2 × 1 = 120

Optimal Solution — O(n) time, O(n) for call stack space

Recursive: n! = n * (n-1)!

class Solution {
    public int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
}

Edge Cases:

  • n = 0: return 1
  • n = 1: return 1
  • n = 12: maximum for int (12! = 479001600)
Fibonacci Number
Binary Recursion

The Fibonacci numbers are defined as F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2). Return F(n).

Example:

Input: n = 5

Output: 5

F(5) = F(4) + F(3) = 3 + 2 = 5

Brute Force Solution — O(2^n) time, O(n) for call stack space

Naive recursion: F(n) = F(n-1) + F(n-2)

class Solution {
    public int fib(int n) {
        if (n <= 1) return n;
        return fib(n - 1) + fib(n - 2);
    }
}
Optimal Solution — O(n) time, O(1) space

Iterative with O(1) space

class Solution {
    public int fib(int n) {
        if (n <= 1) return n;
        int prev2 = 0, prev1 = 1;
        for (int i = 2; i <= n; i++) {
            int curr = prev1 + prev2;
            prev2 = prev1;
            prev1 = curr;
        }
        return prev1;
    }
}

Edge Cases:

  • n = 0: return 0
  • n = 1: return 1
  • n = 2: return 1
Reverse String Recursively
Linear Recursion

Write a recursive function to reverse a string.

Example:

Input: s = "hello"

Output: "olleh"

Reverse of "hello" is "olleh"

Optimal Solution — O(n²) due to string concatenation time, O(n) for call stack space

Recursive: reverse(rest) + firstChar

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: return empty
  • Single character: return same
  • Two characters: swap
Power of Two
Recursion / Math

Given an integer n, return true if it is a power of two (i.e., n = 2^x for some integer x).

Example:

Input: n = 16

Output: true

16 = 2^4

Optimal Solution — O(log n) time, O(log n) for call stack space

Recursive: keep dividing by 2

class Solution {
    public boolean isPowerOfTwo(int n) {
        if (n <= 0) return false;
        if (n == 1) return true;
        if (n % 2 != 0) return false;
        return isPowerOfTwo(n / 2);
    }
}

Edge Cases:

  • n = 0: return false
  • n = 1: return true (2^0)
  • n = negative: return false
Sum of Array Elements
Linear Recursion

Given an array of integers, return the sum using recursion.

Example:

Input: nums = [1, 2, 3, 4, 5]

Output: 15

1 + 2 + 3 + 4 + 5 = 15

Optimal Solution — O(n) time, O(n) for call stack space

Recursive: first + sum(rest)

class Solution {
    public int sumArray(int[] nums) {
        return sumHelper(nums, 0);
    }
    
    private int sumHelper(int[] nums, int index) {
        if (index >= nums.length) return 0;
        return nums[index] + sumHelper(nums, index + 1);
    }
}

Edge Cases:

  • Single element: return that element
  • All zeros: return 0
  • Negative numbers: handle correctly

Quiz

1. What is the base case in this function? ```java int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); } ```

Question 1 options

2. What happens if you forget the base case in a recursive function?

Question 2 options

3. What is the space complexity of a recursive function that calls itself n times?

Question 3 options

4. What is the primary purpose of Recursion?

Question 4 options

Flashcards

Question

What are the two essential components of every recursive function?

Answer

1. Base case: stops the recursion. 2. Recursive case: calls itself with a smaller input.

Question

Why is naive Fibonacci O(2^n)?

Answer

Each call branches into two calls, creating a binary tree of recursive calls. The total number of calls grows exponentially.

Question

When should you use recursion over iteration?

Answer

Use recursion for naturally recursive problems: trees, graphs, divide-and-conquer, and backtracking. Use iteration for simple loops and performance-critical code.

Question

What is Recursion?

Answer

Recursion is a key concept in software engineering.

Question

When to use Recursion?

Answer

Use Recursion when building production systems that require reliability, scalability, and maintainability.

Revision Notes

Key Takeaways

  • 1.Recursion solves problems by breaking them into smaller instances
  • 2.Base case stops recursion; recursive case makes progress toward it
  • 3.Space complexity equals maximum recursion depth
  • 4.Naive recursion can be exponential — use memoization for optimization
  • 5.Every recursive solution can be converted to iteration with an explicit stack

Interview Tips

  • Always identify the base case first
  • Trace through small examples before coding
  • State the recurrence relation for complexity
  • Consider converting to iteration if depth is large
  • Practice drawing the recursion tree

Cheat Sheet

Recursion Cheat Sheet

Template:

ReturnType solve(problem) {
    if (baseCase) return directSolution;
    smallerProblem = reduce(problem);
    result = solve(smallerProblem);
    return combine(result);
}

Patterns:

Pattern Calls Example Time
Linear 1 Factorial O(n)
Binary 2 Fibonacci O(2^n)
Tail 1 (accumulator) Factorial O(n)
Tree 2+ Tree traversal O(n)

Complexity Rules:

  • Space = max recursion depth
  • Time = count total calls × work per call
  • Use recurrence relations for analysis

Common Mistakes:

  1. Missing base case → StackOverflowError
  2. Not making progress → Infinite recursion
  3. Redundant computation → Use memoization
  4. Wrong base case → Incorrect results