diff --git a/src/main/java/com/thealgorithms/datastructures/caches/ARCCache.java b/src/main/java/com/thealgorithms/datastructures/caches/ARCCache.java new file mode 100644 index 000000000000..5185f8583a57 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/caches/ARCCache.java @@ -0,0 +1,132 @@ +package com.thealgorithms.datastructures.caches; + +import java.util.LinkedHashMap; +import java.util.Map; +/** + * Adaptive Replacement Cache (ARC) + *

+ * dynamically adjusts cache size based on recent access patterns. + * It aims to provide better performance compared to traditional caching algorithms + * like LRU (Least Recently Used) and LFU (Least Frequently Used). + * It combines elements of LRU (Least Recently Used) and LFU (Least Frequently Used) algorithms + * to efficiently manage frequently accessed and recently used items, + * optimizing cache performance in changing workload scenarios. + * ... + * @author Adarsh Pandey (...) + * + * @param key type + * @param value type + */ + +public class ARCCache { + private final Map cache; + private final LinkedHashMap usageCounts; + private final int t1Capacity; // Capacity for the t1 cache + private final int b1Capacity; // Capacity for the b1 cache + private int totalCount; + + /** + * This constructor initializes an ARCCache object with the given capacity and initializes other necessary fields + * @param capacity the initial capacity of the cache + * @throws IllegalArgumentException if the capacity is negative + */ + public ARCCache(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("Capacity cannot be negative"); + } + this.cache = new LinkedHashMap<>(); + this.usageCounts = new LinkedHashMap<>(); + this.t1Capacity = capacity / 2; // Capacity for the t1 cache + this.b1Capacity = capacity - t1Capacity; // Capacity for the b1 cache + this.totalCount = 0; + } + + /** + * Returns the total capacity of the cache + * + * @return the total capacity of the cache + */ + private int capacity() { + return t1Capacity + b1Capacity; + } + + /** + * Retrieves the value associated with the given key from the cache. + * If the key is present in the cache, its usage count is incremented. + * + * @param key the key whose associated value is to be retrieved + * @return the value associated with the key, or null if the key is not present in the cache + */ + public V get(K key) { + if (cache.containsKey(key)) { + usageCounts.put(key, usageCounts.getOrDefault(key, 0) + 1); + return cache.get(key); + } + return null; + } + + /** + * Adds the specified key-value pair to the cache. + * If the cache exceeds its capacity after adding the new entry, eviction is performed. + * Updates the usage count for the added key. + * + * @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 (cache.size() >= capacity()) { + evict(); + } + cache.put(key, value); + usageCounts.put(key, 1); + totalCount++; + } + + /** + * Evicts an item from the cache when it exceeds its capacity. + * Implements the Adaptive Replacement Cache (ARC) algorithm logic for eviction. + * Removes the least recently used item based on its usage count. + */ + private void evict() { + if (!cache.isEmpty()) { + K keyToRemove = null; + int minUsageCount = Integer.MAX_VALUE; + for (Map.Entry entry : usageCounts.entrySet()) { + if (entry.getValue() < minUsageCount) { + keyToRemove = entry.getKey(); + minUsageCount = entry.getValue(); + } + } + cache.remove(keyToRemove); + usageCounts.remove(keyToRemove); + totalCount--; + adjustCacheSize(); + } + } + + /** + * Adjust the cache sizes based on t1capacity and b1capacity after eviction from cache + */ + private void adjustCacheSize() { + if (cache.size() > capacity()) { + int excess = cache.size() - capacity(); + int t1Size = cache.size() - b1Capacity; + while (excess > 0 && !cache.isEmpty()) { + K keyToRemove = usageCounts.keySet().iterator().next(); + if (t1Size > t1Capacity || (t1Size > 0 && usageCounts.get(keyToRemove) > 1)) { + cache.remove(keyToRemove); + usageCounts.remove(keyToRemove); + totalCount--; + if (t1Size > 0) { + t1Size--; + } + } else { + cache.remove(keyToRemove); + usageCounts.remove(keyToRemove); + totalCount--; + } + excess--; + } + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/caches/ARCCacheTest.java b/src/test/java/com/thealgorithms/datastructures/caches/ARCCacheTest.java new file mode 100644 index 000000000000..79787125dbf7 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/caches/ARCCacheTest.java @@ -0,0 +1,73 @@ +package com.thealgorithms.datastructures.caches; + +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 ARCCacheTest { + private ARCCache cache; + + @BeforeEach + public void setUp() { + int t1Capacity = 2; + int b1Capacity = 1; + int totalCapacity = t1Capacity + b1Capacity; + cache = new ARCCache<>(totalCapacity); // Set capacity to 3 for testing purposes + } + + @Test + public void testPutAndGet() { + cache.put(1, "Value1"); + cache.put(2, "Value2"); + cache.put(3, "Value3"); + + assertEquals("Value1", cache.get(1)); + assertEquals("Value2", cache.get(2)); + assertEquals("Value3", cache.get(3)); + } + + @Test + public void testEviction() { + cache.put(1, "Value1"); + cache.put(2, "Value2"); + cache.put(3, "Value3"); + + cache.put(4, "Value4"); // This should evict key 1 + + assertNull(cache.get(1)); // Key 1 should have been evicted + assertEquals("Value2", cache.get(2)); // Other keys should still be present + assertEquals("Value3", cache.get(3)); + assertEquals("Value4", cache.get(4)); + } + + @Test + public void nullKeysAndValues() { + cache.put(null, "Value1"); + cache.put(2, null); + + assertEquals("Value1", cache.get(null)); + assertNull(cache.get(2)); + assertNull(cache.get(6)); + } + + @Test + public void testRepeatedGet() { + cache.put(1, "Value1"); + + // Repeated get calls should not affect eviction + cache.get(1); + cache.get(1); + cache.get(1); + + // Adding new elements should still evict old ones + cache.put(2, "Value2"); + cache.put(3, "Value3"); + cache.put(4, "Value4"); + + assertNull(cache.get(2)); // Key 2 should have been evicted + assertEquals("Value1", cache.get(1)); // Other keys should still be present + assertEquals("Value3", cache.get(3)); // Other keys should still be present + } +}