Skip to content

Commit ffef544

Browse files
feat: LRU (Least recently used) cache - different implementation (TheAlgorithms#2783)
* feat: add lru cache * test: add test to cover exception * test: add assert for exception message * review: change int to uint32_t * review: add header for std::uint32_t --------- Co-authored-by: realstealthninja <[email protected]>
1 parent 37a9811 commit ffef544

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed

others/lru_cache2.cpp

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/**
2+
* @file
3+
* @brief Implementation for [LRU Cache]
4+
* (https://en.wikipedia.org/wiki/Cache_replacement_policies#:~:text=Least%20Recently%20Used%20(LRU))
5+
*
6+
* @details
7+
* LRU discards the least recently used value.
8+
* Data structures used - doubly linked list and unordered_map
9+
*
10+
* unordered_map maps the key to the address of the node of the linked list.
11+
* If the element is accessed, the element is moved to the beginning of the
12+
* linked list.
13+
*
14+
* When the cache is full, the last element in the linked list is popped.
15+
*
16+
* @author [Karan Sharma](https://github.com/deDSeC00720)
17+
*/
18+
19+
#include <cassert> // for assert
20+
#include <cstdint> // for std::uint32_t
21+
#include <iostream> // for std::cout
22+
#include <unordered_map> // for std::unordered_map
23+
24+
/**
25+
* @namespace
26+
* @brief Other algorithms
27+
*/
28+
namespace others {
29+
30+
/**
31+
* @namespace
32+
* @brief Cache algorithm
33+
*/
34+
namespace Cache {
35+
36+
/**
37+
* @class
38+
* @brief Node for a doubly linked list with data, prev and next pointers
39+
* @tparam T type of the data of the node
40+
*/
41+
template <typename T>
42+
class D_Node {
43+
public:
44+
T data; ///< data of the node
45+
D_Node<T> *prev; ///< previous node in the doubly linked list
46+
D_Node<T> *next; ///< next node in the doubly linked list
47+
48+
explicit D_Node(T data) : data(data), prev(nullptr), next(nullptr) {}
49+
};
50+
51+
template <typename K, typename V>
52+
using CacheNode = D_Node<std::pair<K, V>>;
53+
54+
/**
55+
* @class
56+
* @brief LRUCache
57+
* @tparam K type of key in the LRU
58+
* @tparam V type of value in the LRU
59+
*/
60+
template <typename K, typename V>
61+
class LRUCache {
62+
CacheNode<K, V> *head; ///< head of the doubly linked list
63+
CacheNode<K, V> *tail; ///< tail of the doubly linked list
64+
std::uint32_t _capacity; ///< maximum capacity of the cache
65+
66+
std::unordered_map<K, CacheNode<K, V> *>
67+
node_map; ///< maps the key to the node address
68+
69+
public:
70+
/**
71+
* @brief Constructor, Initialize the head and tail pointers to nullptr and
72+
* initialize the _capacity of the cache
73+
* @param _capacity Total capacity of the cache
74+
*/
75+
explicit LRUCache(int _capacity)
76+
: head(nullptr), tail(nullptr), _capacity(_capacity) {}
77+
78+
private:
79+
/**
80+
* @brief push the node to the front of the linked list.
81+
* @param node_ptr the node to be pushed
82+
*/
83+
void push_front(CacheNode<K, V> *node_ptr) {
84+
if (!head) {
85+
head = node_ptr;
86+
tail = node_ptr;
87+
return;
88+
}
89+
90+
node_ptr->next = head;
91+
head->prev = node_ptr;
92+
head = node_ptr;
93+
}
94+
95+
/**
96+
* @brief move the existing node in the list to the beginning of the list.
97+
* @param node_ptr node to be moved to the beginning.
98+
*/
99+
void make_recent(CacheNode<K, V> *node_ptr) {
100+
if (head == node_ptr) {
101+
return;
102+
}
103+
104+
CacheNode<K, V> *prev = node_ptr->prev;
105+
CacheNode<K, V> *next = node_ptr->next;
106+
107+
prev->next = next;
108+
if (next) {
109+
next->prev = prev;
110+
} else {
111+
tail = prev;
112+
}
113+
114+
node_ptr->prev = nullptr;
115+
node_ptr->next = nullptr;
116+
push_front(node_ptr);
117+
}
118+
119+
/**
120+
* @brief pop the last node in the linked list.
121+
*/
122+
void pop_back() {
123+
if (!head) {
124+
return;
125+
}
126+
if (head == tail) {
127+
delete head;
128+
head = nullptr;
129+
tail = nullptr;
130+
return;
131+
}
132+
133+
CacheNode<K, V> *temp = tail;
134+
tail = tail->prev;
135+
tail->next = nullptr;
136+
delete temp;
137+
}
138+
139+
public:
140+
/**
141+
* @brief upsert a key-value pair
142+
* @param key key of the key-value pair
143+
* @param value value of the key-value pair
144+
*/
145+
void put(K key, V value) {
146+
// update the value if key already exists
147+
if (node_map.count(key)) {
148+
node_map[key]->data.second = value;
149+
make_recent(node_map[key]);
150+
return;
151+
}
152+
153+
// if the cache is full
154+
// remove the least recently used item
155+
if (node_map.size() == _capacity) {
156+
node_map.erase(tail->data.first);
157+
pop_back();
158+
}
159+
160+
CacheNode<K, V> *newNode = new CacheNode<K, V>({key, value});
161+
162+
node_map[key] = newNode;
163+
push_front(newNode);
164+
}
165+
166+
/**
167+
* @brief get the value of the key-value pair if exists
168+
* @param key key of the key-value pair
169+
* @return the value mapped to the given key
170+
* @exception exception is thrown if the key is not present in the cache
171+
*/
172+
V get(K key) {
173+
if (!node_map.count(key)) {
174+
throw std::runtime_error("key is not present in the cache");
175+
}
176+
177+
// move node to the beginning of the list
178+
V value = node_map[key]->data.second;
179+
make_recent(node_map[key]);
180+
return value;
181+
}
182+
183+
/**
184+
* @brief Returns the number of items present in the cache.
185+
* @return number of items in the cache
186+
*/
187+
int size() const { return node_map.size(); }
188+
189+
/**
190+
* @brief Returns the total capacity of the cache
191+
* @return Total capacity of the cache
192+
*/
193+
int capacity() const { return _capacity; }
194+
195+
/**
196+
* @brief returns whether the cache is empty or not
197+
* @return true if the cache is empty, false otherwise.
198+
*/
199+
bool empty() const { return node_map.empty(); }
200+
201+
/**
202+
* @brief destructs the cache, iterates on the map and deletes every node
203+
* present in the cache.
204+
*/
205+
~LRUCache() {
206+
auto it = node_map.begin();
207+
while (it != node_map.end()) {
208+
delete it->second;
209+
++it;
210+
}
211+
}
212+
};
213+
} // namespace Cache
214+
} // namespace others
215+
216+
/**
217+
* @brief self test implementations
218+
* @return void
219+
*/
220+
static void test() {
221+
others::Cache::LRUCache<int, int> cache(5);
222+
223+
// test the initial state of the cache
224+
assert(cache.size() == 0);
225+
assert(cache.capacity() == 5);
226+
assert(cache.empty());
227+
228+
// test insertion in the cache
229+
cache.put(1, 10);
230+
cache.put(-2, 20);
231+
232+
// test the state of cache after inserting some items
233+
assert(cache.size() == 2);
234+
assert(cache.capacity() == 5);
235+
assert(!cache.empty());
236+
237+
// test getting items from the cache
238+
assert(cache.get(1) == 10);
239+
assert(cache.get(-2) == 20);
240+
241+
cache.put(-3, -30);
242+
cache.put(4, 40);
243+
cache.put(5, -50);
244+
cache.put(6, 60);
245+
246+
// test the state after inserting more items than the capacity
247+
assert(cache.size() == 5);
248+
assert(cache.capacity() == 5);
249+
assert(!cache.empty());
250+
251+
// fetching 1 throws runtime_error
252+
// as 1 was evicted being the least recently used
253+
// when 6 was added
254+
try {
255+
cache.get(1);
256+
} catch (const std::runtime_error &e) {
257+
assert(std::string(e.what()) == "key is not present in the cache");
258+
}
259+
260+
// test retrieval of all items in the cache
261+
assert(cache.get(-2) == 20);
262+
assert(cache.get(-3) == -30);
263+
assert(cache.get(4) == 40);
264+
assert(cache.get(5) == -50);
265+
assert(cache.get(6) == 60);
266+
267+
std::cout << "test - passed\n";
268+
}
269+
270+
/**
271+
* @brief main function
272+
* @return 0 on exit
273+
*/
274+
int main() {
275+
test(); // run the self test implementation
276+
return 0;
277+
}

0 commit comments

Comments
 (0)