Start learning free

LESSON 277 OF 410 · RECURSION

Recursive Fibonacci in Java with Time Complexity O(2^n)

Practice O(2n) Read the full explanation

The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,

F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), for n > 1.

Write a recursive function that given n, returns F(n).

Example 1:

Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.

Example 2:

Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.

Example 3:

Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.

Constraints:
  • 0 <= n <= 30
Note:

Don't worry about the time and space complexities of your algorithm.


Hints:

Inside the function, we should first check the base case: if n is less than 2, then we should return n.

If n >= 2, since F(n) = F(n - 1) + F(n - 2), we should return the sum of the recursive calls for n - 1 and n - 2.
STEP 1 OF 2
    Ctrl + Enter to check

    JAVA · RECURSION

    Recursive Fibonacci in Java, explained

    Understanding the Problem

    The core challenge of this problem is to compute the nth Fibonacci number using a recursive approach. The Fibonacci sequence is a classic example in computer science and mathematics, often used to teach recursion and dynamic programming. The sequence starts with 0 and 1, and each subsequent number is the sum of the previous two.

    Common applications of the Fibonacci sequence include algorithm analysis, financial models, and biological settings like the branching of trees or the arrangement of leaves on a stem.

    Potential pitfalls include misunderstanding the base cases and not handling them correctly, which can lead to infinite recursion or incorrect results.

    Approach

    To solve this problem, we can start with a naive recursive solution. This approach directly translates the mathematical definition of the Fibonacci sequence into code. However, this naive solution is not optimal due to its exponential time complexity.

    Naive Recursive Solution

    The naive recursive solution involves calling the function recursively for the two preceding numbers until the base cases are reached. This approach has a time complexity of O(2^n) because it involves a lot of repeated calculations.

    public class Fibonacci {
        // Naive recursive solution
        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);
        }
    
        public static void main(String[] args) {
            int n = 4;
            System.out.println("Fibonacci number F(" + n + ") is " + fibonacci(n));
        }
    }
    

    While this solution is simple and easy to understand, it is highly inefficient for large values of n due to the repeated calculations of the same subproblems.

    Optimized Solutions

    To optimize the solution, we can use techniques like memoization or dynamic programming to store the results of subproblems and avoid redundant calculations.

    Memoization

    Memoization involves storing the results of expensive function calls and reusing them when the same inputs occur again. This reduces the time complexity to O(n).

    import java.util.HashMap;
    import java.util.Map;
    
    public class FibonacciMemoization {
        private static Map<Integer, Integer> memo = new HashMap<>();
    
        public static int fibonacci(int n) {
            // Base cases
            if (n == 0) return 0;
            if (n == 1) return 1;
            // Check if result is already computed
            if (memo.containsKey(n)) return memo.get(n);
            // Compute and store the result
            int result = fibonacci(n - 1) + fibonacci(n - 2);
            memo.put(n, result);
            return result;
        }
    
        public static void main(String[] args) {
            int n = 4;
            System.out.println("Fibonacci number F(" + n + ") is " + fibonacci(n));
        }
    }
    

    Dynamic Programming

    Dynamic programming involves solving problems by breaking them down into simpler subproblems and storing the results. This approach also has a time complexity of O(n) and is often more space-efficient than memoization.

    public class FibonacciDP {
        public static int fibonacci(int n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            int[] dp = new int[n + 1];
            dp[0] = 0;
            dp[1] = 1;
            for (int i = 2; i <= n; i++) {
                dp[i] = dp[i - 1] + dp[i - 2];
            }
            return dp[n];
        }
    
        public static void main(String[] args) {
            int n = 4;
            System.out.println("Fibonacci number F(" + n + ") is " + fibonacci(n));
        }
    }
    

    Complexity Analysis

    Let's analyze the time and space complexity of each approach:

    • Naive Recursive Solution: Time Complexity: O(2^n), Space Complexity: O(n) (due to the call stack).
    • Memoization: Time Complexity: O(n), Space Complexity: O(n) (for the memoization table).
    • Dynamic Programming: Time Complexity: O(n), Space Complexity: O(n) (for the dp array).

    Edge Cases

    Potential edge cases include:

    • n = 0: The function should return 0.
    • n = 1: The function should return 1.
    • n = 30: The function should handle the upper constraint efficiently.

    Testing these edge cases ensures that the function handles all possible inputs correctly.

    Testing

    To test the solution comprehensively, we can use a variety of test cases:

    • Simple cases: n = 0, n = 1, n = 2.
    • Intermediate cases: n = 10, n = 15.
    • Upper bound cases: n = 30.

    We can use testing frameworks like JUnit to automate the testing process.

    Thinking and Problem-Solving Tips

    When approaching such problems, it's essential to:

    • Understand the problem statement and constraints thoroughly.
    • Start with a simple, naive solution to get a basic understanding.
    • Identify inefficiencies and think about optimization techniques.
    • Practice solving similar problems to improve problem-solving skills.

    Conclusion

    In this blog post, we discussed the problem of computing the nth Fibonacci number using a recursive approach. We explored the naive recursive solution and its limitations, followed by optimized solutions using memoization and dynamic programming. We also analyzed the complexity of each approach and discussed edge cases and testing strategies.

    Understanding and solving such problems is crucial for developing strong problem-solving skills and a deep understanding of algorithms. We encourage readers to practice and explore further to enhance their skills.

    Additional Resources

    For further reading and practice problems related to the Fibonacci sequence and recursion, consider the following resources:

    Keep going with JavaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free