diff --git a/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java b/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java index 4e233224e367..f0d8ea8f7ff3 100644 --- a/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java +++ b/src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java @@ -6,16 +6,21 @@ /** * 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 The type of keys maintained by this cache. - * @param The type of mapped values. + *

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.

* *

* Reference: LFU Cache - Wikipedia *

* + * @param The type of keys maintained by this cache. + * @param The type of mapped values. + * * @author Akshay Dubey (https://github.com/itsAkshayDubey) */ public class LFUCache { @@ -75,7 +80,7 @@ public LFUCache(int capacity) { /** * 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. diff --git a/src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java index 6a94345d625e..8ad927564aa5 100644 --- a/src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java +++ b/src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java @@ -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 @@ void testLFUCacheWithIntegerValueShouldPass() { 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 @@ void testLFUCacheWithIntegerValueShouldPass() { // 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 @@ void testLFUCacheWithStringValueShouldPass() { 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 @@ void testLFUCacheWithStringValueShouldPass() { 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 @@ void testLFUCacheWithStringValueShouldPass() { // 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 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 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 lfuCache = new LFUCache<>(3); + assertNull(lfuCache.get(1)); // Should return null as the cache is empty + } + + @Test + void testPutNullValueShouldStoreNull() { + LFUCache 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 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 } }