Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and add.
Implement the LRUCache class:
LRUCache(int capacity) Initialize the LRU cache with positive size capacity.
int get(int key) Return the value of the key if the key exists, otherwise return -1.
void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.
The functions get and put must each run in O(1) average time complexity.
Since it’s a cache you need to guarantee a O(1) for reading and writing. This hints at using hash table. We also need some form ordering, so Queue is a FIFO Data structure. We use two data structures to implement an LRU Cache.
Queue which is implemented using a doubly linked list. The maximum size of the queue will be equal to the total number of frames available (cache size).The most recently used (MRU) node will be near tail end and least recently (LRU) used node will be near head end. (In queue, we remove values from front, but we can also do it from tail.)
Hash Table with page number as key and address of the corresponding queue node as value.
There are a couple of solutions to this problem. Here are some we will cover:
publicclassLRUCache {
staticclassDLLNodeKV {
int key;
int value;
DLLNodeKV prev;
DLLNodeKV next;
publicDLLNodeKV(int key, int value) {
this.key= key;
this.value= value;
}
}
privatefinalint capacity;
privatefinal Map <Integer,DLLNodeKV > map;
DLLNodeKV head =null;
DLLNodeKV tail =null;
publicLRUCache(int capacity) {
this.capacity= capacity;
this.map=new HashMap<>();
}
publicintget(int key) {
if(!map.containsKey(key)){
return-1;
}
DLLNodeKV n = map.get(key);
remove(n);
offerAtTail(n);
return n.value;
}
publicvoidput(int key, int value) {
if (map.containsKey(key)) {
DLLNodeKV old = map.get(key);
old.value= value;
remove(old);
offerAtTail(old);
} else {
if (map.size() >= capacity) {
map.remove(head.key);//this is why we want to store key in dll node remove(head);
}
DLLNodeKV created =new DLLNodeKV(key, value);
offerAtTail(created);
map.put(key, created);
}
}
privatevoidremove(DLLNodeKV n) {
// if not head nodeif (n.prev!=null) {
n.prev.next= n.next;
} else {
head = n.next;
}
// if not tail nodeif (n.next!=null) {
n.next.prev= n.prev;
} else {
tail = n.prev;
}
}
privatevoidofferAtTail(DLLNodeKV n) {
if (tail !=null) {
tail.next= n;
}
n.prev= tail;
n.next=null;
tail = n;
if (head ==null) {
head = tail;
}
}
}
Of course you probably shouldn’t implement your own cache or linked list, there are so many very efficient implementations.
In Java you can implement your LRUCache in a couple of instructions reusing java built in data structures but here we just want to understand what’s going on and write our own for learning purpose.
Notice also that this implementation is not thread safe too.