Skip to content

Commit e3582d8

Browse files
Merge branch 'master' into master
2 parents 4e33044 + be0b1d5 commit e3582d8

File tree

4 files changed

+268
-52
lines changed

4 files changed

+268
-52
lines changed

src/main/java/com/thealgorithms/datastructures/caches/LFUCache.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66
/**
77
* The {@code LFUCache} class implements a Least Frequently Used (LFU) cache.
88
* An LFU cache evicts the least frequently used item when the cache reaches its capacity.
9-
* It keeps track of how many times each item is used and maintains a doubly linked list
10-
* for efficient addition and removal of items based on their frequency of use.
9+
* It maintains a mapping of keys to nodes, where each node contains the key, its associated value,
10+
* and a frequency count that tracks how many times the item has been accessed. A doubly linked list
11+
* is used to efficiently manage the ordering of items based on their usage frequency.
1112
*
12-
* @param <K> The type of keys maintained by this cache.
13-
* @param <V> The type of mapped values.
13+
* <p>This implementation is designed to provide O(1) time complexity for both the {@code get} and
14+
* {@code put} operations, which is achieved through the use of a hashmap for quick access and a
15+
* doubly linked list for maintaining the order of item frequencies.</p>
1416
*
1517
* <p>
1618
* Reference: <a href="https://en.wikipedia.org/wiki/Least_frequently_used">LFU Cache - Wikipedia</a>
1719
* </p>
1820
*
21+
* @param <K> The type of keys maintained by this cache.
22+
* @param <V> The type of mapped values.
23+
*
1924
* @author Akshay Dubey (https://github.com/itsAkshayDubey)
2025
*/
2126
public class LFUCache<K, V> {
@@ -75,7 +80,7 @@ public LFUCache(int capacity) {
7580

7681
/**
7782
* Retrieves the value associated with the given key from the cache.
78-
* If the key exists, the node's frequency is increased and the node is repositioned
83+
* If the key exists, the node's frequency is incremented, and the node is repositioned
7984
* in the linked list based on its updated frequency.
8085
*
8186
* @param key The key whose associated value is to be returned.

src/main/java/com/thealgorithms/datastructures/caches/LRUCache.java

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,40 @@
44
import java.util.Map;
55

66
/**
7-
* Least recently used (LRU)
8-
* <p>
9-
* Discards the least recently used items first. This algorithm requires keeping
10-
* track of what was used when, which is expensive if one wants to make sure the
11-
* algorithm always discards the least recently used item.
12-
* https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
7+
* A Least Recently Used (LRU) Cache implementation.
138
*
14-
* @param <K> key type
15-
* @param <V> value type
9+
* <p>An LRU cache is a fixed-size cache that maintains items in order of use. When the cache reaches
10+
* its capacity and a new item needs to be added, it removes the least recently used item first.
11+
* This implementation provides O(1) time complexity for both get and put operations.</p>
12+
*
13+
* <p>Features:</p>
14+
* <ul>
15+
* <li>Fixed-size cache with configurable capacity</li>
16+
* <li>Constant time O(1) operations for get and put</li>
17+
* <li>Thread-unsafe - should be externally synchronized if used in concurrent environments</li>
18+
* <li>Supports null values but not null keys</li>
19+
* </ul>
20+
*
21+
* <p>Implementation Details:</p>
22+
* <ul>
23+
* <li>Uses a HashMap for O(1) key-value lookups</li>
24+
* <li>Maintains a doubly-linked list for tracking access order</li>
25+
* <li>The head of the list contains the least recently used item</li>
26+
* <li>The tail of the list contains the most recently used item</li>
27+
* </ul>
28+
*
29+
* <p>Example usage:</p>
30+
* <pre>
31+
* LRUCache<String, Integer> cache = new LRUCache<>(3); // Create cache with capacity 3
32+
* cache.put("A", 1); // Cache: A=1
33+
* cache.put("B", 2); // Cache: A=1, B=2
34+
* cache.put("C", 3); // Cache: A=1, B=2, C=3
35+
* cache.get("A"); // Cache: B=2, C=3, A=1 (A moved to end)
36+
* cache.put("D", 4); // Cache: C=3, A=1, D=4 (B evicted)
37+
* </pre>
38+
*
39+
* @param <K> the type of keys maintained by this cache
40+
* @param <V> the type of mapped values
1641
*/
1742
public class LRUCache<K, V> {
1843

@@ -30,6 +55,11 @@ public LRUCache(int cap) {
3055
setCapacity(cap);
3156
}
3257

58+
/**
59+
* Returns the current capacity of the cache.
60+
*
61+
* @param newCapacity the new capacity of the cache
62+
*/
3363
private void setCapacity(int newCapacity) {
3464
checkCapacity(newCapacity);
3565
for (int i = data.size(); i > newCapacity; i--) {
@@ -39,6 +69,11 @@ private void setCapacity(int newCapacity) {
3969
this.cap = newCapacity;
4070
}
4171

72+
/**
73+
* Evicts the least recently used item from the cache.
74+
*
75+
* @return the evicted entry
76+
*/
4277
private Entry<K, V> evict() {
4378
if (head == null) {
4479
throw new RuntimeException("cache cannot be empty!");
@@ -50,12 +85,25 @@ private Entry<K, V> evict() {
5085
return evicted;
5186
}
5287

88+
/**
89+
* Checks if the capacity is valid.
90+
*
91+
* @param capacity the capacity to check
92+
*/
5393
private void checkCapacity(int capacity) {
5494
if (capacity <= 0) {
5595
throw new RuntimeException("capacity must greater than 0!");
5696
}
5797
}
5898

99+
/**
100+
* Returns the value to which the specified key is mapped, or null if this cache contains no
101+
* mapping for the key.
102+
*
103+
* @param key the key whose associated value is to be returned
104+
* @return the value to which the specified key is mapped, or null if this cache contains no
105+
* mapping for the key
106+
*/
59107
public V get(K key) {
60108
if (!data.containsKey(key)) {
61109
return null;
@@ -65,6 +113,11 @@ public V get(K key) {
65113
return entry.getValue();
66114
}
67115

116+
/**
117+
* Moves the specified entry to the end of the list.
118+
*
119+
* @param entry the entry to move
120+
*/
68121
private void moveNodeToLast(Entry<K, V> entry) {
69122
if (tail == entry) {
70123
return;
@@ -86,6 +139,12 @@ private void moveNodeToLast(Entry<K, V> entry) {
86139
tail = entry;
87140
}
88141

142+
/**
143+
* Associates the specified value with the specified key in this cache.
144+
*
145+
* @param key the key with which the specified value is to be associated
146+
* @param value the value to be associated with the specified key
147+
*/
89148
public void put(K key, V value) {
90149
if (data.containsKey(key)) {
91150
final Entry<K, V> existingEntry = data.get(key);
@@ -107,6 +166,11 @@ public void put(K key, V value) {
107166
data.put(key, newEntry);
108167
}
109168

169+
/**
170+
* Adds a new entry to the end of the list.
171+
*
172+
* @param newEntry the entry to add
173+
*/
110174
private void addNewEntry(Entry<K, V> newEntry) {
111175
if (data.isEmpty()) {
112176
head = newEntry;

src/test/java/com/thealgorithms/datastructures/caches/LFUCacheTest.java

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.thealgorithms.datastructures.caches;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
46

57
import org.junit.jupiter.api.Test;
68

@@ -22,15 +24,15 @@ void testLFUCacheWithIntegerValueShouldPass() {
2224
lfuCache.put(6, 60);
2325

2426
// will return null as value with key 2 is now evicted
25-
assertEquals(null, lfuCache.get(2));
27+
assertNull(lfuCache.get(2));
2628

2729
// should return 60
2830
assertEquals(60, lfuCache.get(6));
2931

3032
// this operation will remove value with key as 3
3133
lfuCache.put(7, 70);
3234

33-
assertEquals(null, lfuCache.get(2));
35+
assertNull(lfuCache.get(2));
3436
assertEquals(70, lfuCache.get(7));
3537
}
3638

@@ -41,7 +43,7 @@ void testLFUCacheWithStringValueShouldPass() {
4143
lfuCache.put(2, "Beta");
4244
lfuCache.put(3, "Gamma");
4345
lfuCache.put(4, "Delta");
44-
lfuCache.put(5, "Eplison");
46+
lfuCache.put(5, "Epsilon");
4547

4648
// get method call will increase frequency of key 1 by 1
4749
assertEquals("Alpha", lfuCache.get(1));
@@ -50,33 +52,87 @@ void testLFUCacheWithStringValueShouldPass() {
5052
lfuCache.put(6, "Digamma");
5153

5254
// will return null as value with key 2 is now evicted
53-
assertEquals(null, lfuCache.get(2));
55+
assertNull(lfuCache.get(2));
5456

5557
// should return string Digamma
5658
assertEquals("Digamma", lfuCache.get(6));
5759

5860
// this operation will remove value with key as 3
5961
lfuCache.put(7, "Zeta");
6062

61-
assertEquals(null, lfuCache.get(2));
63+
assertNull(lfuCache.get(2));
6264
assertEquals("Zeta", lfuCache.get(7));
6365
}
6466

65-
/**
66-
* test addNodeWithUpdatedFrequency method
67-
* @author yuluo
68-
*/
6967
@Test
70-
void testAddNodeWithUpdatedFrequency() {
68+
void testUpdateValueShouldPreserveFrequency() {
7169
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
72-
lfuCache.put(1, "beijing");
73-
lfuCache.put(2, "shanghai");
74-
lfuCache.put(3, "gansu");
70+
lfuCache.put(1, "A");
71+
lfuCache.put(2, "B");
72+
lfuCache.put(3, "C");
7573

76-
assertEquals("beijing", lfuCache.get(1));
74+
assertEquals("A", lfuCache.get(1)); // Accessing key 1
75+
lfuCache.put(4, "D"); // This should evict key 2
7776

78-
lfuCache.put(1, "shanxi");
77+
assertNull(lfuCache.get(2)); // Key 2 should be evicted
78+
assertEquals("C", lfuCache.get(3)); // Key 3 should still exist
79+
assertEquals("A", lfuCache.get(1)); // Key 1 should still exist
7980

80-
assertEquals("shanxi", lfuCache.get(1));
81+
lfuCache.put(1, "Updated A"); // Update the value of key 1
82+
assertEquals("Updated A", lfuCache.get(1)); // Check if the update was successful
83+
}
84+
85+
@Test
86+
void testEvictionPolicyWhenFull() {
87+
LFUCache<Integer, String> lfuCache = new LFUCache<>(2);
88+
lfuCache.put(1, "One");
89+
lfuCache.put(2, "Two");
90+
91+
assertEquals("One", lfuCache.get(1)); // Access key 1
92+
lfuCache.put(3, "Three"); // This should evict key 2 (least frequently used)
93+
94+
assertNull(lfuCache.get(2)); // Key 2 should be evicted
95+
assertEquals("One", lfuCache.get(1)); // Key 1 should still exist
96+
assertEquals("Three", lfuCache.get(3)); // Check if key 3 exists
97+
}
98+
99+
@Test
100+
void testGetFromEmptyCacheShouldReturnNull() {
101+
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
102+
assertNull(lfuCache.get(1)); // Should return null as the cache is empty
103+
}
104+
105+
@Test
106+
void testPutNullValueShouldStoreNull() {
107+
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
108+
lfuCache.put(1, null); // Store a null value
109+
110+
assertNull(lfuCache.get(1)); // Should return null
111+
}
112+
113+
@Test
114+
void testInvalidCacheCapacityShouldThrowException() {
115+
assertThrows(IllegalArgumentException.class, () -> new LFUCache<>(0));
116+
assertThrows(IllegalArgumentException.class, () -> new LFUCache<>(-1));
117+
}
118+
119+
@Test
120+
void testMultipleAccessPatterns() {
121+
LFUCache<Integer, String> lfuCache = new LFUCache<>(5);
122+
lfuCache.put(1, "A");
123+
lfuCache.put(2, "B");
124+
lfuCache.put(3, "C");
125+
lfuCache.put(4, "D");
126+
127+
assertEquals("A", lfuCache.get(1)); // Access 1
128+
lfuCache.put(5, "E"); // Should not evict anything yet
129+
lfuCache.put(6, "F"); // Evict B
130+
131+
assertNull(lfuCache.get(2)); // B should be evicted
132+
assertEquals("C", lfuCache.get(3)); // C should still exist
133+
assertEquals("D", lfuCache.get(4)); // D should still exist
134+
assertEquals("A", lfuCache.get(1)); // A should still exist
135+
assertEquals("E", lfuCache.get(5)); // E should exist
136+
assertEquals("F", lfuCache.get(6)); // F should exist
81137
}
82138
}

0 commit comments

Comments
 (0)