-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2bd3a6f
commit 467930c
Showing
3 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# LFUCache4j | ||
|
||
LFUCache4j is a thread-safe implementation of a Least Frequently Used (LFU) cache in Java. This cache supports generic | ||
key-value pairs and ensures that the least frequently accessed elements are evicted first when the cache reaches its | ||
capacity. | ||
|
||
## Features | ||
|
||
- **Thread-Safe**: Utilizes a ReentrantLock to ensure thread safety. | ||
- **LFU Eviction Policy**: Evicts the least frequently used elements first. | ||
- **Generic**: Supports generic types for keys and values. | ||
|
||
## Usage | ||
|
||
### Creating an LFU Cache | ||
|
||
```java | ||
LFUCache4j<Integer, String> cache = new LFUCache4j<>(2); | ||
``` | ||
|
||
### Adding Elements to the Cache | ||
|
||
```java | ||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
``` | ||
|
||
### Retrieving Elements from the Cache | ||
|
||
```java | ||
String value1 = cache.get(1); // Returns "one" | ||
String value2 = cache.get(2); // Returns "two" | ||
``` | ||
|
||
### Updating an Element in the Cache | ||
|
||
```java | ||
cache.put(1, "ONE"); // Updates the value associated with key 1 | ||
String updatedValue = cache.get(1); // Returns "ONE" | ||
``` | ||
|
||
### Handling Cache Eviction | ||
|
||
When the cache reaches its capacity, the least frequently used element is evicted to make space for new elements. | ||
|
||
```java | ||
cache.put(3, "three"); // Evicts the least frequently used element | ||
String evictedValue = cache.get(2); // Returns null, as element with key 2 is evicted | ||
``` | ||
|
||
## API Reference | ||
|
||
### Constructor | ||
|
||
- `LFUCache4j(int capacity)`: Creates a new LFU cache with the specified capacity. | ||
|
||
### Methods | ||
|
||
- `V get(K key)`: Retrieves the value associated with the specified key. Updates the access frequency of the key. Returns null if the key is not found. | ||
- `void put(K key, V value)`: Inserts the specified key-value pair into the cache. If the cache is at capacity, the least frequently used item is evicted. If the key already exists, its value is updated and its frequency is incremented. | ||
|
||
## Example | ||
|
||
```java | ||
public class Main { | ||
public static void main(String[] args) { | ||
LFUCache4j<Integer, String> cache = new LFUCache4j<>(2); | ||
|
||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
|
||
System.out.println(cache.get(1)); // Output: one | ||
System.out.println(cache.get(2)); // Output: two | ||
|
||
cache.put(3, "three"); // Evicts key 2 | ||
|
||
System.out.println(cache.get(2)); // Output: null | ||
System.out.println(cache.get(3)); // Output: three | ||
|
||
cache.put(1, "ONE"); // Updates value for key 1 | ||
|
||
System.out.println(cache.get(1)); // Output: ONE | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package org.alpha4j.ds; | ||
|
||
import java.util.HashMap; | ||
import java.util.LinkedHashSet; | ||
import java.util.Map; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
|
||
/** | ||
* LFUCache4j is a thread-safe implementation of the Least Frequently Used (LFU) cache. | ||
* It supports generic key-value pairs and ensures that the least frequently accessed | ||
* elements are evicted first when the cache reaches its capacity. | ||
* | ||
* @param <K> the type of keys maintained by this cache | ||
* @param <V> the type of mapped values | ||
*/ | ||
public class LFUCache4j<K, V> { | ||
protected final ReentrantLock lock = new ReentrantLock(); // Define a lock to ensure thread safety | ||
protected int capacity; // Cache capacity | ||
protected int size = 0; // Current size of the cache | ||
protected Map<K, V> cache; // Map to store keys and their corresponding values | ||
protected Map<K, Integer> frequencies; // Map to store keys and their corresponding frequencies | ||
protected Map<Integer, LinkedHashSet<K>> frequencyIndexes; // Map to store frequencies and the corresponding sets of keys | ||
protected int minFrequency; // Variable to keep track of the minimum frequency | ||
|
||
/** | ||
* Constructor to initialize the LFUCache4j with a specific capacity. | ||
* | ||
* @param capacity the maximum number of items that can be held in the cache | ||
*/ | ||
public LFUCache4j(int capacity) { | ||
this.capacity = capacity; | ||
this.cache = new HashMap<>(); | ||
this.frequencies = new HashMap<>(); | ||
this.frequencyIndexes = new HashMap<>(); | ||
this.minFrequency = 1; | ||
} | ||
|
||
/** | ||
* Retrieves the value associated with the specified key from the cache. | ||
* If the key is not found, returns null. This method also updates the | ||
* frequency of the accessed key. | ||
* | ||
* @param key the key whose associated value is to be returned | ||
* @return the value associated with the specified key, or null if the key is not found | ||
*/ | ||
@SuppressWarnings({"UnusedReturnValue"}) | ||
public V get(K key) { | ||
try { | ||
lock.lock(); | ||
if (!cache.containsKey(key)) { | ||
return null; | ||
} | ||
int frequency = frequencies.get(key); // Get the current frequency of the key | ||
frequencyIndexes.get(frequency).remove(key); // Remove the key from the current frequency list | ||
// If the current frequency list is empty and the frequency equals minFrequency, increment minFrequency | ||
if (frequencyIndexes.get(frequency).isEmpty() && frequency == minFrequency) { | ||
minFrequency++; | ||
} | ||
frequency++; // Increment the frequency and update the data structures | ||
frequencies.put(key, frequency); | ||
frequencyIndexes.computeIfAbsent(frequency, k -> new LinkedHashSet<>()).add(key); | ||
return cache.get(key); | ||
} finally { | ||
lock.unlock(); | ||
} | ||
} | ||
|
||
/** | ||
* Inserts the specified key-value pair into the cache. If the cache is at capacity, | ||
* the least frequently used item will be removed to make space for the new entry. | ||
* If the key already exists, its value will be updated and its frequency will be incremented. | ||
* | ||
* @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) { | ||
try { | ||
lock.lock(); | ||
if (capacity <= 0) { | ||
return; | ||
} | ||
if (cache.containsKey(key)) { | ||
cache.put(key, value); // Update the value and increase the frequency | ||
this.get(key); // Update the frequency by calling get | ||
return; | ||
} | ||
if (size >= capacity) { | ||
// Remove the least frequently used element | ||
K evict = frequencyIndexes.get(minFrequency).iterator().next(); | ||
frequencyIndexes.get(minFrequency).remove(evict); | ||
cache.remove(evict); | ||
frequencies.remove(evict); | ||
size--; | ||
} | ||
// Add the new key and value | ||
cache.put(key, value); | ||
frequencies.put(key, 1); | ||
frequencyIndexes.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key); | ||
minFrequency = 1; // Reset minFrequency to 1 for the new entry | ||
size++; | ||
} finally { | ||
lock.unlock(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package org.alpha4j; | ||
|
||
import org.alpha4j.ds.LFUCache4j; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNull; | ||
|
||
public class LFUCache4jTest { | ||
|
||
private LFUCache4j<Integer, String> cache; | ||
|
||
@Before | ||
public void setUp() { | ||
// Initialize the cache with a capacity of 2 | ||
cache = new LFUCache4j<>(2); | ||
} | ||
|
||
@Test | ||
public void testPutAndGet() { | ||
// Test adding elements to the cache | ||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
|
||
// Verify the elements are correctly added | ||
assertEquals("one", cache.get(1)); | ||
assertEquals("two", cache.get(2)); | ||
|
||
// Test updating an existing element | ||
cache.put(1, "ONE"); | ||
assertEquals("ONE", cache.get(1)); | ||
} | ||
|
||
@Test | ||
public void testEviction() { | ||
// Add elements to fill the cache | ||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
|
||
// Access element 1 to increase its frequency | ||
cache.get(1); | ||
|
||
// Add another element to trigger eviction | ||
cache.put(3, "three"); | ||
|
||
// Verify that element 2 is evicted (as it was less frequently accessed) | ||
assertNull(cache.get(2)); | ||
assertEquals("one", cache.get(1)); | ||
assertEquals("three", cache.get(3)); | ||
} | ||
|
||
@Test | ||
public void testFrequencyUpdate() { | ||
// Add elements to the cache | ||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
|
||
// Access elements to update their frequencies | ||
cache.get(1); | ||
cache.get(1); | ||
cache.get(2); | ||
|
||
// Add another element to trigger eviction | ||
cache.put(3, "three"); | ||
|
||
// Verify that element 2 is evicted (since element 1 has higher frequency) | ||
assertNull(cache.get(2)); | ||
assertEquals("one", cache.get(1)); | ||
assertEquals("three", cache.get(3)); | ||
} | ||
|
||
@Test | ||
public void testUpdateValue() { | ||
// Add an element to the cache | ||
cache.put(1, "one"); | ||
|
||
// Verify the value is correctly added | ||
assertEquals("one", cache.get(1)); | ||
|
||
// Update the value of the existing element | ||
cache.put(1, "ONE"); | ||
|
||
// Verify the value is correctly updated | ||
assertEquals("ONE", cache.get(1)); | ||
} | ||
|
||
@Test | ||
public void testMinFrequency() { | ||
// Add elements to the cache | ||
cache.put(1, "one"); | ||
cache.put(2, "two"); | ||
|
||
// Access elements to update their frequencies | ||
cache.get(1); | ||
cache.get(2); | ||
cache.get(2); | ||
|
||
// Add another element to trigger eviction | ||
cache.put(3, "three"); | ||
|
||
// Verify that element 1 is evicted (since it has the lowest frequency) | ||
assertNull(cache.get(1)); | ||
assertEquals("two", cache.get(2)); | ||
assertEquals("three", cache.get(3)); | ||
} | ||
|
||
@Test | ||
public void testZeroCapacity() { | ||
// Initialize the cache with zero capacity | ||
LFUCache4j<Integer, String> zeroCapacityCache = new LFUCache4j<>(0); | ||
|
||
// Attempt to add elements to the zero-capacity cache | ||
zeroCapacityCache.put(1, "one"); | ||
|
||
// Verify that no elements are added | ||
assertNull(zeroCapacityCache.get(1)); | ||
} | ||
} |