An LRU or "Least Recently Used" cache is a data structure that is used for storing a fixed size list of items ordered by the frequency at which they are accessed.
This comes up quite often as an interview question, and though it's possible to
implement the solution using an Array
, the interviewer will most likely ask
you to implement both get
and put
operations in such a way that the time
complexity is constant time - O(1)
.
To solve this, we can use a Map
as it offers us similar functionality to an
Array
, but with some performance benefits.
Difference between Map and Object
JavaScript Map's are similar to Objects, with a few exceptions:
- The keys of a
Map
are ordered by the order of insertion. - Listing the keys of a map is an constant time operation, where
Object.keys(obj)
is anO(n)
operation where n is the number of keys in the object. - Getting the size of a
Map
is anO(1)
operation (map.size
), whereObject.keys(obj).length
is anO(n)
operation.
Implementing the cache
We'll begin by creating a new LRUCache
class, with 2 static properties:
- The capacity of the cache
- The cache itself
class LRUCache {
constructor(capacity) {
// Store the capacity size
this.capacity = capacity
// Store the cache as a Map
this.cache = new Map()
}
}
Next we'll implement a get
method on the class for retrieving a value in our
cache by a particular key. If the key is not present in our cache, we should
return -1.
get(key) {
// Key does not exist, return -1
if (!this.cache.has(key)) {
return -1
}
// Temporarily store the value
const value = this.cache.get(key)
// Delete the key
this.cache.delete(key)
// Reinsert the key,value
this.cache.set(key, value)
// Return the value
return value
}
You might be wondering why we're deleting the key, every time we access it and
the reason for this is that, as mentioned previously, a Map
stores elements by
the order at which they were inserted. So in this case, we can move our
key,value pair to the top of the cache by deleting it and reinserting it -
putting in the position of the most frequently accessed item.
put(key, value) {
// If the key already exists, delete it so that it will be added
// to the top of the cache
if (this.cache.get(key)) {
this.cache.delete(key)
}
// Insert the key,value pair into cache
this.cache.set(key, value)
// If we've exceeded the cache capacity,
// then delete the least recently accessed value,
// which will be the item at the bottom of the cache
// i.e the first position
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
}
Note here that map.keys()
is not the same as Object.keys(obj)
. Where
Object.keys(obj)
returns an array of keys, map.keys()
is a generator
function which returns a MapIterator
. For this reason, we need to access the
first value by calling .next().value
to access the first value in the
MapIterator
of keys.
Using our class
const cache = new LRUCache(3)
// Insert 3 items to our cache
cache.set(1, 10) // Map { 1 => 10 }
cache.set(2, 20) // Map { 1 => 10, 2 => 20 }
cache.set(3, 30) // Map { 1 => 10, 2 => 20, 3 => 30 }
// Fetch by key
cache.get(2) // Returns 20, Map { 1 => 10, 3 => 30, 2 => 20 }
// Insert another item
cache.set(4, 40) // Map { 3 => 30, 2 => 20, 4 => 40 }
// Notice how our "1" key has been evicted