The problem
Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.
Implement the LRUCache
class:
LRUCache(int capacity)
Initialize the LRU cache with positive sizecapacity
.int get(int key)
Return the value of thekey
if the key exists, otherwise return-1
.void put(int key, int value)
Update the value of thekey
if thekey
exists. Otherwise, add thekey-value
pair to the cache. If the number of keys exceeds thecapacity
from this operation, evict the least recently used key.
The functions get
and put
must each run in O(1)
average time complexity.
Examples
Input
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
Output
[null, null, null, 1, null, -1, null, -1, 3, 4]
Explanation
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // cache is {1=1}
lRUCache.put(2, 2); // cache is {1=1, 2=2}
lRUCache.get(1); // return 1
lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3}
lRUCache.get(2); // returns -1 (not found)
lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3}
lRUCache.get(1); // return -1 (not found)
lRUCache.get(3); // return 3
lRUCache.get(4); // return 4
Constraints
- 1 <= capacity <= 3000
- 0 <= key <= 10^4
- 0 <= value <= 10^5
- At most 2 * 10^5 calls will be made to get and put.
Explanation
Let’s look at our example:
- From the first operation we get our capacity that equals
2
. - Next, we have
put
operation with pairs[1, 1]
. We will be keeping added values in order because we want to remember in what order we added these values. - In the next operation we
put
pairs[2, 2]
. - At the next operation we are trying to
get
LRU value with key1
.
The way we are going to do this is by using a hashmap, because we can look up the key
in O(1)
time. We also are not going to create any additional memory, because the size of our hashmap will not be exceeding our capacity
.
We will be using our hashmap by adding the key
that was given in our input; as for value
, we will be using a pointer to the node.
When we execute our third get
operation, we will be returning value 1
. When we executed the get
operation, we made our pairs [1, 1]
the most frequently used and pairs [2, 2]
the least frequently used.
We are going to keep track of the most recent element and least frequently used element by using two pointers left
and right
, where the left
pointer will be storing the LRU
element and the right
pointer will be storing the most recent element. Therefore, we are going to swap
these two nodes.
Now we need to reorder our two nodes. The nodes will be connected with each other because we are using a doubly linked list that will allow us to swap nodes in O(1)
time.
Next, we are going to execute our fourth put
operation and put pairs [3, 3]
to our hashmap. At this point we have a capacity of 2
, and when we try to add a new key, we exceed it, so we need to remove the LRU
value and pointer to it and replace it with the new key 3
and create a new pointer.
We will continue executing steps and follow the algorithm above until we reach the end of our input.
Doubly Linked List Solution
final class Node {
let key: Int
let val: Int
var prev: Node?
var next: Node?
init(_ key: Int, _ val: Int) {
self.key = key
self.val = val
}
}
class LRUCache {
private let capacity: Int
private var cache: [Int: Node]
private var left: Node
private var right: Node
init(_ capacity: Int) {
self.capacity = capacity
self.cache = [:]
self.left = Node(0, 0)
self.right = Node(0, 0)
self.left.next = self.right
self.right.prev = self.left
}
func remove(_ node: Node) {
let prev = node.prev
let nxt = node.next
prev?.next = nxt
nxt?.prev = prev
}
func insert(_ node: Node) {
let prev = self.right.prev
let nxt = self.right
prev?.next = node
nxt.prev = node
node.next = nxt
node.prev = prev
}
func get(_ key: Int) -> Int {
if let node = self.cache[key] {
self.remove(node)
self.insert(node)
return node.val
}
return -1
}
func put(_ key: Int, _ value: Int) {
if self.cache[key] != nil {
self.remove(self.cache[key]!)
}
self.cache[key] = Node(key, value)
self.insert(self.cache[key]!)
if self.cache.count > self.capacity {
let lru = self.left.next!
self.remove(lru)
self.cache.removeValue(forKey: lru.key)
}
}
}
Explanation
To solve this problem we will need an additional class Node
that will be storing key
, value
pairs, and we are also going to have pointers prev
and next
that will point to the previous and next Node
accordingly.
In our hashmap, which we will call cache
, we will be mapping our key
to Node
.
We will also need left
and right
pointers that will store our LRU
and most recent
values.
When we call our get
operation, we will be returning our value if it exists in our cache
. We also should not forget to update our LRU
and most recent
pointers after we get
the value. To do that, we will need two helper functions β remove
, and insert
β where remove
will be removing the node from the list and insert
will be inserting the node to the right.
When we call the put
operation and the key
already exists in our cache
, we will remove that node and replace it with a new one.
Lastly, if our cache
count exceeds the given capacity
, we will need to remove the LRU
value from the list and our cache
.
Time/ Space complexity
- Time complexity: O(1) for each
put
andget
operation - Space complexity: O(n)