Enhance docs, add more tests in LFUCache (#5949)

This commit is contained in:
Hardik Pawar
2024-10-23 23:51:03 +05:30
committed by GitHub
parent b64e53cd3d
commit be0b1d58d6
2 changed files with 82 additions and 21 deletions

View File

@ -6,16 +6,21 @@ import java.util.Map;
/**
* The {@code LFUCache} class implements a Least Frequently Used (LFU) cache.
* An LFU cache evicts the least frequently used item when the cache reaches its capacity.
* It keeps track of how many times each item is used and maintains a doubly linked list
* for efficient addition and removal of items based on their frequency of use.
* It maintains a mapping of keys to nodes, where each node contains the key, its associated value,
* and a frequency count that tracks how many times the item has been accessed. A doubly linked list
* is used to efficiently manage the ordering of items based on their usage frequency.
*
* @param <K> The type of keys maintained by this cache.
* @param <V> The type of mapped values.
* <p>This implementation is designed to provide O(1) time complexity for both the {@code get} and
* {@code put} operations, which is achieved through the use of a hashmap for quick access and a
* doubly linked list for maintaining the order of item frequencies.</p>
*
* <p>
* Reference: <a href="https://en.wikipedia.org/wiki/Least_frequently_used">LFU Cache - Wikipedia</a>
* </p>
*
* @param <K> The type of keys maintained by this cache.
* @param <V> The type of mapped values.
*
* @author Akshay Dubey (https://github.com/itsAkshayDubey)
*/
public class LFUCache<K, V> {
@ -75,7 +80,7 @@ public class LFUCache<K, V> {
/**
* Retrieves the value associated with the given key from the cache.
* If the key exists, the node's frequency is increased and the node is repositioned
* If the key exists, the node's frequency is incremented, and the node is repositioned
* in the linked list based on its updated frequency.
*
* @param key The key whose associated value is to be returned.

View File

@ -1,6 +1,8 @@
package com.thealgorithms.datastructures.caches;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
@ -22,7 +24,7 @@ class LFUCacheTest {
lfuCache.put(6, 60);
// will return null as value with key 2 is now evicted
assertEquals(null, lfuCache.get(2));
assertNull(lfuCache.get(2));
// should return 60
assertEquals(60, lfuCache.get(6));
@ -30,7 +32,7 @@ class LFUCacheTest {
// this operation will remove value with key as 3
lfuCache.put(7, 70);
assertEquals(null, lfuCache.get(2));
assertNull(lfuCache.get(2));
assertEquals(70, lfuCache.get(7));
}
@ -41,7 +43,7 @@ class LFUCacheTest {
lfuCache.put(2, "Beta");
lfuCache.put(3, "Gamma");
lfuCache.put(4, "Delta");
lfuCache.put(5, "Eplison");
lfuCache.put(5, "Epsilon");
// get method call will increase frequency of key 1 by 1
assertEquals("Alpha", lfuCache.get(1));
@ -50,7 +52,7 @@ class LFUCacheTest {
lfuCache.put(6, "Digamma");
// will return null as value with key 2 is now evicted
assertEquals(null, lfuCache.get(2));
assertNull(lfuCache.get(2));
// should return string Digamma
assertEquals("Digamma", lfuCache.get(6));
@ -58,25 +60,79 @@ class LFUCacheTest {
// this operation will remove value with key as 3
lfuCache.put(7, "Zeta");
assertEquals(null, lfuCache.get(2));
assertNull(lfuCache.get(2));
assertEquals("Zeta", lfuCache.get(7));
}
/**
* test addNodeWithUpdatedFrequency method
* @author yuluo
*/
@Test
void testAddNodeWithUpdatedFrequency() {
void testUpdateValueShouldPreserveFrequency() {
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
lfuCache.put(1, "beijing");
lfuCache.put(2, "shanghai");
lfuCache.put(3, "gansu");
lfuCache.put(1, "A");
lfuCache.put(2, "B");
lfuCache.put(3, "C");
assertEquals("beijing", lfuCache.get(1));
assertEquals("A", lfuCache.get(1)); // Accessing key 1
lfuCache.put(4, "D"); // This should evict key 2
lfuCache.put(1, "shanxi");
assertNull(lfuCache.get(2)); // Key 2 should be evicted
assertEquals("C", lfuCache.get(3)); // Key 3 should still exist
assertEquals("A", lfuCache.get(1)); // Key 1 should still exist
assertEquals("shanxi", lfuCache.get(1));
lfuCache.put(1, "Updated A"); // Update the value of key 1
assertEquals("Updated A", lfuCache.get(1)); // Check if the update was successful
}
@Test
void testEvictionPolicyWhenFull() {
LFUCache<Integer, String> lfuCache = new LFUCache<>(2);
lfuCache.put(1, "One");
lfuCache.put(2, "Two");
assertEquals("One", lfuCache.get(1)); // Access key 1
lfuCache.put(3, "Three"); // This should evict key 2 (least frequently used)
assertNull(lfuCache.get(2)); // Key 2 should be evicted
assertEquals("One", lfuCache.get(1)); // Key 1 should still exist
assertEquals("Three", lfuCache.get(3)); // Check if key 3 exists
}
@Test
void testGetFromEmptyCacheShouldReturnNull() {
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
assertNull(lfuCache.get(1)); // Should return null as the cache is empty
}
@Test
void testPutNullValueShouldStoreNull() {
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
lfuCache.put(1, null); // Store a null value
assertNull(lfuCache.get(1)); // Should return null
}
@Test
void testInvalidCacheCapacityShouldThrowException() {
assertThrows(IllegalArgumentException.class, () -> new LFUCache<>(0));
assertThrows(IllegalArgumentException.class, () -> new LFUCache<>(-1));
}
@Test
void testMultipleAccessPatterns() {
LFUCache<Integer, String> lfuCache = new LFUCache<>(5);
lfuCache.put(1, "A");
lfuCache.put(2, "B");
lfuCache.put(3, "C");
lfuCache.put(4, "D");
assertEquals("A", lfuCache.get(1)); // Access 1
lfuCache.put(5, "E"); // Should not evict anything yet
lfuCache.put(6, "F"); // Evict B
assertNull(lfuCache.get(2)); // B should be evicted
assertEquals("C", lfuCache.get(3)); // C should still exist
assertEquals("D", lfuCache.get(4)); // D should still exist
assertEquals("A", lfuCache.get(1)); // A should still exist
assertEquals("E", lfuCache.get(5)); // E should exist
assertEquals("F", lfuCache.get(6)); // F should exist
}
}