Skip to content

Commit ab7c20c

Browse files
committed
feat: add lru cache
1 parent dd36279 commit ab7c20c

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed

others/lru_cache2.cpp

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

0 commit comments

Comments
 (0)