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:
+ *
+ * - Fixed-size cache with configurable capacity
+ * - Constant time O(1) operations for get and put
+ * - Thread-unsafe - should be externally synchronized if used in concurrent environments
+ * - Supports null values but not null keys
+ *
+ *
+ * Implementation Details:
+ *
+ * - Uses a HashMap for O(1) key-value lookups
+ * - Maintains a doubly-linked list for tracking access order
+ * - The head of the list contains the least recently used item
+ * - The tail of the list contains the most recently used item
+ *
+ *
+ * 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));
}
}