Last Updated: 29 Aug, 2020

LRU Cache Implementation

Moderate
Asked in companies
MicrosoftUberSalesforce

Problem statement

Design and implement a data structure for Least Recently Used (LRU) cache to support the following operations:

1. get(key) - Return the value of the key if the key exists in the cache, otherwise return -1.

2. put(key, value), Insert the value in the cache if the key is not already present or update the value of the given key if the key is already present. When the cache reaches its capacity, it should invalidate the least recently used item before inserting the new item.
You will be given ‘Q’ queries. Each query will belong to one of these two types:
Type 0: for get(key) operation.
Type 1: for put(key, value) operation.
Note :
1. The cache is initialized with a capacity (the maximum number of unique keys it can hold at a time).

2. Access to an item or key is defined as a get or a put operation on the key. The least recently used key is the one with the oldest access time.
Input Format :
The first line of input contains two space-separated integers 'C' and 'Q', denoting the capacity of the cache and the number of operations to be performed respectively.

The next Q lines contain operations, one per line. Each operation starts with an integer which represents the type of operation. 

If it is 0, then it is of the first type and is followed by one integer key. 

If it is 1, it is of the second type and is followed by two space-separated integers key and value(in this order). 
Output Format :
For each operation of type 0, print an integer on a single line, denoting the value of the key if the key exists, otherwise -1.
Note :
You don't need to print anything, it has already been taken care of. Just implement the given function.
Constraints :
1 <= C <= 10^4
1 <= Q <= 10^5
1 <= key, value <= 10^9

Time Limit: 1 sec
Sample Input 1 :
3 11
1 1 1
1 2 2
1 3 3
1 4 5
0 3
0 1
0 4
1 2 3
0 1
0 3
0 2
Sample Output 1 :
3
-1
5
-1
3
3
Explanation to Sample Input 1 :

alt-text

Initializing a cache of capacity 3, LRUCache cache = new LRUCache(3);
Then each operation is performed as shown in the above figure.
cache.put(1,1)
cache.put(2,2)
cache.put(3,3)
cache.put(4,5)
cache.get(3)      // returns 3
cache.get(1)      // returns -1
cache.get(2)      // returns 2
cache.put(5,5)
cache.get(4)      // returns -1
cache.get(3)      // returns 3
Sample Input 2 :
2 6
1 1 1
1 2 2
0 2
1 3 3
0 3
0 1
Sample Output 2 :
2
3
-1

Approaches

01 Approach

We will use an array of type Pair<key, value> to implement our LRU Cache where the larger the index is, the more recently the key is used. Means, the 0th index denotes the least recently used pair, and the last index denotes the most recently used pair.

 

The key will be considered as accessed if we try to perform any operation on it. So while performing the get operation on a key, we will do a linear search, if the key found in the cache at an index id we will left shift all keys starting from id+1 in the cache by one and put the key at the end (marking it as the most recently used key in the cache). 

 

Similarly, while performing the put operation on a key, we will do a linear search, if the key found at index equals to id, we will shift left all keys starting from id+1 in the cache by one and put the key at the end. Otherwise, we will check the current size of the cache. If the size equals capacity, we will remove the first(0th) key from the cache by doing the left shift on all the keys by one. Now we can simply insert the key at the end.

 

size: denotes the current size of the cache.

capacity: denotes the maximum keys cache can hold at one time.

cache: denotes the array of type pair to store key-value pairs.

Pair: Pair type will store these values, key, value.

 

Algorithm

 

This method will return the index of the key if it exists in the cache, otherwise -1.

 

getIndex(int key):

  • For all the pairs in the cache, from i = 0 to size - 1.
    • If currentPair->key == key, return i.
  • Return -1, as the given key does not exist in the cache.

This method will shift left all the pairs starting from the start index by 1.

 

leftShift(int start):

  • If start == size, no pair exists to shift, return.
  • For all pairs in tha cache, from i = start to size - 1,
    • cache[i - 1] = cache[i]

 

int get(key):

  • ID = getIndex(key).
  • If ID == -1, the key does not exist in the cache, return -1.
  • Get the pair at index = ID, let it pair.
  • leftShift(ID + 1).
  • cache[size - 1] = pair
  • Return pair->value.

 

void put(key, value):

  • ID = getIndex(key).
  • If ID is not equal to -1, the key already exists in the cache,
    • Get the pair at index = ID, let it pair.
    • leftShift(ID + 1).
    • pair->value = value.
    • cache[size - 1] = pair
    • Return.
  • Create a new pair of (key, value), let it be pair.
  • If the size is less than the capacity,
    • Insert pair in cache at index = size, cache[size] = pair
    • Increment size by 1.
  • Otherwise,
    • Remove the first pair from the cache, by performing a left shift, leftShift(1).

Insert pair in cache at index = size - 1, cache[size - 1] = 

pair.

02 Approach

We will use two data structures to implement our LRU Cache.

 

  1. Queue<Node>: To store the nodes into cache where the least recently used key will be the head node and the most recently used key will be the tail node.
  2. Map<K, Node>: To map the keys to the corresponding nodes.

 

The key will be considered as accessed if we try to perform any operation on it. So while performing the get operation on a key, if the key already exists in the cache we will detach the key from the cache and put it at the tail end (marking it as the most recently used key in the cache). Similarly, while performing the put operation on a key, if the key already exists, we will detach it from the cache and put it at the tail end. Otherwise, we will put the key at the tail end and check the current size of the cache. If the size exceeds capacity, we will remove the head key from the cache.

 

To perform both of these operations at the minimum cost (O(1)). We will use a doubly-linked list to implement our queue.

 

head: head node of the queue, to store least recently used key-value pair.

tail: tail node of the queue, to store the most recently used key-value pair.

size: denotes the current size of the cache.

capacity: denotes the maximum keys cache can hold at one time.

Node: Node type will store these values, left node, right node, key, and value.

 

Algorithm

 

addToRear(Node node)

  • If head == null, head = node.
  • Else tail->right = node and node->left = tail
  • tail = node

 

deleteNode(Node node)

  • If the given node is the head node, head = head->right.
  • Else node->left->right = node->right.
  • If the given node is the tail node, tail = tail.left
  • Else node->right->left = node.left
  • node->right = null.
  • node->left = null

 

int get(key)

 

  • If the key does not exist, return -1.
  • Fetch the node corresponding to the given key from the map, let it be nod.
  • deleteNode(nod).
  • addToRear(nod).
  • Return nod->value.

 

void put(key, value)

 

  • If the key already exists in the cache, remove the key from the cache and put it at the tail end with the new value.
    • Fetch the node corresponding to the given key from the map, let it be nod.
    • nod->value = value.
    • deleteNode(nod)
    • addToRear(nod)
    • return
  • Initialize a new node with the given key-value pair. Let it nod, nod = node(key,value).
    • map.put(key, nod).
    • If the cache size reaches its capacity,
      • map.remove(key).
      • deleteNode(nod)
      • addToRear(nod)
    • Else,
      • addToRear(nod)
      • Increment size by 1.