mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-07-05 00:14:33 +08:00
Enhance docs, add more tests in LRUCache
(#5950)
This commit is contained in:
@ -4,15 +4,40 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Least recently used (LRU)
|
||||
* <p>
|
||||
* 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 <K> key type
|
||||
* @param <V> value type
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Features:</p>
|
||||
* <ul>
|
||||
* <li>Fixed-size cache with configurable capacity</li>
|
||||
* <li>Constant time O(1) operations for get and put</li>
|
||||
* <li>Thread-unsafe - should be externally synchronized if used in concurrent environments</li>
|
||||
* <li>Supports null values but not null keys</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Implementation Details:</p>
|
||||
* <ul>
|
||||
* <li>Uses a HashMap for O(1) key-value lookups</li>
|
||||
* <li>Maintains a doubly-linked list for tracking access order</li>
|
||||
* <li>The head of the list contains the least recently used item</li>
|
||||
* <li>The tail of the list contains the most recently used item</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example usage:</p>
|
||||
* <pre>
|
||||
* LRUCache<String, Integer> 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)
|
||||
* </pre>
|
||||
*
|
||||
* @param <K> the type of keys maintained by this cache
|
||||
* @param <V> the type of mapped values
|
||||
*/
|
||||
public class LRUCache<K, V> {
|
||||
|
||||
@ -30,6 +55,11 @@ public class LRUCache<K, V> {
|
||||
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 @@ public class LRUCache<K, V> {
|
||||
this.cap = newCapacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts the least recently used item from the cache.
|
||||
*
|
||||
* @return the evicted entry
|
||||
*/
|
||||
private Entry<K, V> evict() {
|
||||
if (head == null) {
|
||||
throw new RuntimeException("cache cannot be empty!");
|
||||
@ -50,12 +85,25 @@ public class LRUCache<K, V> {
|
||||
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 class LRUCache<K, V> {
|
||||
return entry.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the specified entry to the end of the list.
|
||||
*
|
||||
* @param entry the entry to move
|
||||
*/
|
||||
private void moveNodeToLast(Entry<K, V> entry) {
|
||||
if (tail == entry) {
|
||||
return;
|
||||
@ -86,6 +139,12 @@ public class LRUCache<K, V> {
|
||||
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<K, V> existingEntry = data.get(key);
|
||||
@ -107,6 +166,11 @@ public class LRUCache<K, V> {
|
||||
data.put(key, newEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new entry to the end of the list.
|
||||
*
|
||||
* @param newEntry the entry to add
|
||||
*/
|
||||
private void addNewEntry(Entry<K, V> newEntry) {
|
||||
if (data.isEmpty()) {
|
||||
head = newEntry;
|
||||
|
@ -3,56 +3,147 @@ 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 LRUCacheTest {
|
||||
|
||||
private static final int SIZE = 5;
|
||||
private LRUCache<Integer, Integer> cache;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cache = new LRUCache<>(SIZE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void putAndGetIntegerValues() {
|
||||
LRUCache<Integer, Integer> 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 testAccessOrder() {
|
||||
// Fill cache
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
cache.put(i, 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 testNullValues() {
|
||||
cache.put(1, null);
|
||||
assertNull(cache.get(1));
|
||||
|
||||
// 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<String, String> 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 putAndGetStringValues() {
|
||||
LRUCache<String, String> lruCache = new LRUCache<>(SIZE);
|
||||
void testCustomObjects() {
|
||||
class TestObject {
|
||||
private final String value;
|
||||
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
lruCache.put("key" + i, "value" + 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();
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
assertEquals("value" + i, lruCache.get("key" + i));
|
||||
}
|
||||
}
|
||||
LRUCache<Integer, TestObject> objectCache = new LRUCache<>(SIZE);
|
||||
TestObject obj1 = new TestObject("test1");
|
||||
TestObject obj2 = new TestObject("test2");
|
||||
|
||||
@Test
|
||||
public void nullKeysAndValues() {
|
||||
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
|
||||
mruCache.put(null, 2);
|
||||
mruCache.put(6, null);
|
||||
objectCache.put(1, obj1);
|
||||
objectCache.put(2, obj2);
|
||||
|
||||
assertEquals(2, mruCache.get(null));
|
||||
assertNull(mruCache.get(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overCapacity() {
|
||||
LRUCache<Integer, Integer> mruCache = new LRUCache<>(SIZE);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
mruCache.put(i, i);
|
||||
}
|
||||
|
||||
assertEquals(9, mruCache.get(9));
|
||||
assertEquals(obj1, objectCache.get(1));
|
||||
assertEquals(obj2, objectCache.get(2));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user