diff --git a/src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java b/src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java index 97818ff83351..ec39d2a6ed28 100644 --- a/src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java +++ b/src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java @@ -4,15 +4,40 @@ import java.util.Map; /** - * Least recently used (LRU) - *

- * Discards the least recently used items first. This algorithm requires keeping - * track of what was used when, which is expensive if one wants to make sure the - * algorithm always discards the least recently used item. - * https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) + * A Least Recently Used (LRU) Cache implementation. * - * @param key type - * @param value type + *

An LRU cache is a fixed-size cache that maintains items in order of use. When the cache reaches + * its capacity and a new item needs to be added, it removes the least recently used item first. + * This implementation provides O(1) time complexity for both get and put operations.

+ * + *

Features:

+ * + * + *

Implementation Details:

+ * + * + *

Example usage:

+ *
+ * LRUCache cache = new LRUCache<>(3); // Create cache with capacity 3
+ * cache.put("A", 1); // Cache: A=1
+ * cache.put("B", 2); // Cache: A=1, B=2
+ * cache.put("C", 3); // Cache: A=1, B=2, C=3
+ * cache.get("A");    // Cache: B=2, C=3, A=1 (A moved to end)
+ * cache.put("D", 4); // Cache: C=3, A=1, D=4 (B evicted)
+ * 
+ * + * @param the type of keys maintained by this cache + * @param the type of mapped values */ public class LRUCache { @@ -30,6 +55,11 @@ public LRUCache(int cap) { setCapacity(cap); } + /** + * Returns the current capacity of the cache. + * + * @param newCapacity the new capacity of the cache + */ private void setCapacity(int newCapacity) { checkCapacity(newCapacity); for (int i = data.size(); i > newCapacity; i--) { @@ -39,6 +69,11 @@ private void setCapacity(int newCapacity) { this.cap = newCapacity; } + /** + * Evicts the least recently used item from the cache. + * + * @return the evicted entry + */ private Entry evict() { if (head == null) { throw new RuntimeException("cache cannot be empty!"); @@ -50,12 +85,25 @@ private Entry evict() { return evicted; } + /** + * Checks if the capacity is valid. + * + * @param capacity the capacity to check + */ private void checkCapacity(int capacity) { if (capacity <= 0) { throw new RuntimeException("capacity must greater than 0!"); } } + /** + * Returns the value to which the specified key is mapped, or null if this cache contains no + * mapping for the key. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or null if this cache contains no + * mapping for the key + */ public V get(K key) { if (!data.containsKey(key)) { return null; @@ -65,6 +113,11 @@ public V get(K key) { return entry.getValue(); } + /** + * Moves the specified entry to the end of the list. + * + * @param entry the entry to move + */ private void moveNodeToLast(Entry entry) { if (tail == entry) { return; @@ -86,6 +139,12 @@ private void moveNodeToLast(Entry entry) { tail = entry; } + /** + * Associates the specified value with the specified key in this cache. + * + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + */ public void put(K key, V value) { if (data.containsKey(key)) { final Entry existingEntry = data.get(key); @@ -107,6 +166,11 @@ public void put(K key, V value) { data.put(key, newEntry); } + /** + * Adds a new entry to the end of the list. + * + * @param newEntry the entry to add + */ private void addNewEntry(Entry newEntry) { if (data.isEmpty()) { head = newEntry; diff --git a/src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java index c56ada060022..99b9952435c4 100644 --- a/src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java +++ b/src/test/java/com/thealgorithms/datastructures/caches/LRUCacheTest.java @@ -3,56 +3,147 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class LRUCacheTest { - private static final int SIZE = 5; + private LRUCache cache; + + @BeforeEach + void setUp() { + cache = new LRUCache<>(SIZE); + } @Test - public void putAndGetIntegerValues() { - LRUCache lruCache = new LRUCache<>(SIZE); + public void testBasicOperations() { + cache.put(1, 100); + assertEquals(100, cache.get(1)); + assertNull(cache.get(2)); + } + @Test + public void testEvictionPolicy() { + // Fill cache to capacity for (int i = 0; i < SIZE; i++) { - lruCache.put(i, i); + cache.put(i, i * 100); } + // Verify all elements are present for (int i = 0; i < SIZE; i++) { - assertEquals(i, lruCache.get(i)); + assertEquals(i * 100, cache.get(i)); } + + // Add one more element, causing eviction of least recently used + cache.put(SIZE, SIZE * 100); + + // First element should be evicted + assertNull(cache.get(0)); + assertEquals(SIZE * 100, cache.get(SIZE)); } @Test - public void putAndGetStringValues() { - LRUCache lruCache = new LRUCache<>(SIZE); - + public void testAccessOrder() { + // Fill cache for (int i = 0; i < SIZE; i++) { - lruCache.put("key" + i, "value" + i); + cache.put(i, i); } - for (int i = 0; i < SIZE; i++) { - assertEquals("value" + i, lruCache.get("key" + i)); - } + // Access first element, making it most recently used + cache.get(0); + + // Add new element, should evict second element (1) + cache.put(SIZE, SIZE); + + assertEquals(0, cache.get(0)); // Should still exist + assertNull(cache.get(1)); // Should be evicted + assertEquals(SIZE, cache.get(SIZE)); // Should exist + } + + @Test + public void testUpdateExistingKey() { + cache.put(1, 100); + assertEquals(100, cache.get(1)); + + // Update existing key + cache.put(1, 200); + assertEquals(200, cache.get(1)); } @Test - public void nullKeysAndValues() { - LRUCache mruCache = new LRUCache<>(SIZE); - mruCache.put(null, 2); - mruCache.put(6, null); + public void testNullValues() { + cache.put(1, null); + assertNull(cache.get(1)); - assertEquals(2, mruCache.get(null)); - assertNull(mruCache.get(6)); + // Update null to non-null + cache.put(1, 100); + assertEquals(100, cache.get(1)); + + // Update non-null to null + cache.put(1, null); + assertNull(cache.get(1)); + } + + @Test + public void testStringKeysAndValues() { + LRUCache stringCache = new LRUCache<>(SIZE); + + stringCache.put("key1", "value1"); + stringCache.put("key2", "value2"); + + assertEquals("value1", stringCache.get("key1")); + assertEquals("value2", stringCache.get("key2")); + } + + @Test + public void testLongSequenceOfOperations() { + // Add elements beyond capacity multiple times + for (int i = 0; i < SIZE * 3; i++) { + cache.put(i, i * 100); + + // Verify only the last SIZE elements are present + for (int j = Math.max(0, i - SIZE + 1); j <= i; j++) { + assertEquals(j * 100, cache.get(j)); + } + + // Verify elements before the window are evicted + if (i >= SIZE) { + assertNull(cache.get(i - SIZE)); + } + } } @Test - public void overCapacity() { - LRUCache mruCache = new LRUCache<>(SIZE); + void testCustomObjects() { + class TestObject { + private final String value; - for (int i = 0; i < 10; i++) { - mruCache.put(i, i); + TestObject(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TestObject) { + return value.equals(((TestObject) obj).value); + } + return false; + } + + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } } - assertEquals(9, mruCache.get(9)); + LRUCache objectCache = new LRUCache<>(SIZE); + TestObject obj1 = new TestObject("test1"); + TestObject obj2 = new TestObject("test2"); + + objectCache.put(1, obj1); + objectCache.put(2, obj2); + + assertEquals(obj1, objectCache.get(1)); + assertEquals(obj2, objectCache.get(2)); } }