LRU Cache medium
Problem Statement
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 the key 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.
Example 1
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
Example 2
Input
["LRUCache", "put", "get", "put", "get", "get"]
[[1], [2, 1], [2], [3, 2], [2], [3]]
Output
[null, null, 1, null, -1, 2]
Explanation
LRUCache lRUCache = new LRUCache(1);
lRUCache.put(2, 1); // cache is {2=1}
lRUCache.get(2); // return 1
lRUCache.put(3, 2); // LRU key was 2, evicts key 2, cache is {3=2}
lRUCache.get(2); // returns -1 (not found)
lRUCache.get(3); // return 2
Steps
-
Data Structure: Use a combination of a
Map
(for O(1) key lookup) and aDoubly Linked List
(for O(1) insertion and deletion at both ends). The Map will store{key: Node}
pairs, whereNode
is a node in the doubly linked list. The doubly linked list maintains the order of usage, with the most recently used node at the head and the least recently used node at the tail. -
get(key)
: Check if the key exists in the map. If it does, move the corresponding node to the head of the list (marking it as most recently used) and return its value. If it doesn't, return -1. -
put(key, value)
: Check if the key exists. If it does, update its value and move the node to the head. If it doesn't, create a new node and add it to the head. If the cache is full, remove the tail node (LRU) from the list and the map.
Explanation
The key to achieving O(1) time complexity for both get
and put
operations is using a doubly linked list to manage the order of elements and a hash map for quick lookups. The doubly linked list allows efficient insertion and deletion at both ends, while the hash map enables O(1) access to nodes based on their keys. When a key is accessed or updated, its corresponding node is moved to the head of the list, ensuring that the most recently used elements are always at the front.
Code
class Node {
key: number;
value: number;
prev: Node | null = null;
next: Node | null = null;
constructor(key: number, value: number) {
this.key = key;
this.value = value;
}
}
class LRUCache {
capacity: number;
map: Map<number, Node>;
head: Node | null = null;
tail: Node | null = null;
constructor(capacity: number) {
this.capacity = capacity;
this.map = new Map();
}
get(key: number): number {
if (!this.map.has(key)) return -1;
const node = this.map.get(key)!;
this.moveToHead(node);
return node.value;
}
put(key: number, value: number): void {
if (this.map.has(key)) {
const node = this.map.get(key)!;
node.value = value;
this.moveToHead(node);
} else {
const newNode = new Node(key, value);
this.addToHead(newNode);
this.map.set(key, newNode);
if (this.map.size > this.capacity) {
this.removeTail();
}
}
}
addToHead(node: Node) {
node.next = this.head;
if (this.head) this.head.prev = node;
this.head = node;
if (!this.tail) this.tail = node;
}
removeNode(node: Node) {
if (node.prev) node.prev.next = node.next;
if (node.next) node.next.prev = node.prev;
if (this.head === node) this.head = node.next;
if (this.tail === node) this.tail = node.prev;
}
removeTail() {
if (!this.tail) return;
const tail = this.tail;
this.removeNode(tail);
this.map.delete(tail.key);
}
moveToHead(node: Node) {
this.removeNode(node);
this.addToHead(node);
}
}
Complexity
-
Time Complexity:
get
: O(1) - Map lookup and list manipulation are constant time.put
: O(1) - Map operations and list manipulations are constant time.
-
Space Complexity: O(capacity) - The space used is proportional to the capacity of the cache.