Java Memoization Techniques: Enhance Code Performance with Caching

Java Cache Memoization Algorithms

In Java, we can use memoization with a HashMap to cache expensive function calculations so we retrieve the value without having to perform the expensive calculations again. Memoization is a specific form of caching that is employed at the level of function calls.

When a function is memoized, before executing the function body, the function will check whether the result for the given set of input parameters is already present in the cache (often implemented as a hash table or a dictionary). If the result is in the cache, the function returns the cached result without having to perform the calculation again. If the result is not in the cache, the function proceeds to compute the result, stores this result in the cache, and then returns it.

Fibonacci Algorithm with and without Memoization

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. In recursive programming, the Fibonacci sequence can be implemented by a function that calls itself to calculate the previous two numbers in the sequence. However, without memoization, this approach can be quite inefficient for larger numbers due to the exponential growth of function calls.

Here’s a simple Java method that demonstrates a recursive Fibonacci sequence without memoization:

public class Fibonacci {

    public static int fibonacci(int n) {
        if(n <= 0) {
            return 0;
        } else if(n == 1) {
            return 1;
        } else {
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }

    public static void main(String[] args) {
        System.out.println(fibonacci(10)); // Should print the 10th Fibonacci number
    }
}

This method works well for small values of n, but it becomes increasingly slow for larger values because it recalculates the same Fibonacci numbers many times. Memoization (caching the results of function calls) is often used to improve the efficiency of recursive Fibonacci implementations.

This way to solve the Fibonacci problem has an exponential time complexity of O(2^n) due to the repeated computation of the same subproblems. To learn more about time complexity, check out the article: Big (O) Notation Explanation

Fibonacci with Memoization

Now, let’s implement a memoized Fibonacci sequence by using a HashMap to store the results of the Fibonacci numbers that have already been computed. Here’s an example of how we code this:

import java.util.HashMap;
import java.util.Map;

public class FibonacciMemoization {

    private static Map<Integer, Long> memo = new HashMap<>();

    public static long fibonacci(int n) {
        if (n <= 1) {
            return n;
        }
        
        // Return cached result if it exists
        if (memo.containsKey(n)) {
            return memo.get(n);
        }
        
        // Compute and cache the result if it's not already in the cache
        long result = fibonacci(n - 1) + fibonacci(n - 2);
        memo.put(n, result);
        
        return result;
    }

    public static void main(String[] args) {
        int n = 50; // Will calculate the 50th Fibonacci number
        System.out.println("Fibonacci number at position " + n + " is " + fibonacci(n));
    }
}

In this example:

The HashMap named memo is used to store the previously computed Fibonacci numbers.
The fibonacci function first checks whether the result for the given n is already present in the memo. If it is, it returns the result immediately, avoiding the expensive recursive calls.

If the result is not in the cache, the function computes the result, stores it in the memo, and then returns it.
By using this approach, the time complexity of calculating the nth Fibonacci number is reduced to O(n), because each Fibonacci number from 0 to n is calculated only once. This makes the algorithm significantly more efficient for large values of n.

Memoization with Factorial

The factorial of a non-negative integer n, denoted by n!, is the product of all positive integers less than or equal to n. The factorial function is often used in permutations and combinations and can be defined recursively as:

0! = 1 (by definition)
n! = n × (n-1)! for n > 0

Calculating factorials recursively without memoization is inefficient for large n because it involves recomputing the factorial of numbers multiple times. By using memoization, each factorial value is computed only once. When using memoization or not, we will have the time complexity of O(n), but by using memoization we will significantly reduce the number of calculations performed.

Here’s an example of a memoized factorial function in Java:

import java.util.HashMap;
import java.util.Map;

public class FactorialMemoization {

    private static Map<Integer, Long> memo = new HashMap<>();

    public static long factorial(int n) {
        // Base case: 0! = 1
        if (n == 0) {
            return 1;
        }

        // Check if the value is already in the memo
        if (memo.containsKey(n)) {
            return memo.get(n);
        }

        // Recursive calculation with memoization
        long result = n * factorial(n - 1);
        memo.put(n, result);

        return result;
    }

    public static void main(String[] args) {
        int n = 10; // Calculate the factorial of 10
        System.out.println(n + "! = " + factorial(n));
    }
}

In this implementation:

A HashMap named memo is used to store the previously computed factorials.
The factorial function checks if the result for the given n is already in the memo. If it is, it returns the result immediately.
If the result is not in the cache, the function computes the result, stores it in the memo, and then returns it.
With memoization, the time complexity for calculating the factorial of n is reduced to O(n), because the function is called exactly n times for each unique input, and each call does constant work aside from the recursive call. This makes the algorithm much more efficient, especially for large values of n.

Conclusion

Memoization dramatically enhances the efficiency of recursive algorithms such as Fibonacci and factorial by caching computed results, reducing their time complexity from exponential to linear and ensuring that each expensive function call is executed only once for large input values. Let’s recap some crucial points of this article:

  • Memoization is a technique to speed up programs by caching the results of expensive function calls.
  • It works by checking if the function’s result for given input parameters is already in the cache before executing the function body.
  • If the result is cached, the function returns it immediately; otherwise, it computes, caches, and returns the result.
  • Recursive Fibonacci without memoization is inefficient for large n due to exponential time complexity O(2^n).
  • Memoized Fibonacci uses a HashMap to store computed values, reducing time complexity to linear O(n).
  • Memoized Fibonacci checks for cached results before performing recursive calculations, computing each Fibonacci number only once.
  • Factorials computed recursively without memoization is also inefficient for large n.
  • Memoized factorial uses a HashMap to store computed factorials, ensuring each factorial is computed once.
  • This memoization reduces the factorial time complexity to O(n), significantly improving perfomance by doing fewer calculations.
Written by
Rafael del Nero
Join the discussion