Baozi Training Leetcode solution 146. LRU Cache
Problem Statement
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get
and put
.
get(key)
- Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value)
- Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
The cache is initialized with a positive capacity.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
LRUCache cache = new LRUCache( 2 /* capacity */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // returns 1 cache.put(3, 3); // evicts key 2 cache.get(2); // returns -1 (not found) cache.put(4, 4); // evicts key 1 cache.get(1); // returns -1 (not found) cache.get(3); // returns 3 cache.get(4); // returns 4
Problem link
Video Tutorial
You can find the detailed video tutorial here
Thought Process
The LRU cache has become an absolute standard interview question that we have to know how Java's LinkedHashMap or Python's OrderedDict works under the cover (A doubly linked list and a hashmap). I would actually recommend implementing a doubly linked list + hashmap solution in real interviews.
Follow up is how to implement a least frequent cache (LFU)?
- A doubly linkedlist + hashmap + A min heap (to count), however, both get and put would be O(LgN) since the heap would be modified due to the frequency increased by 1
- A doubly linkedlist + hashmap + ArrayList<Frequency, doubly linked list), Node structure would also carry a Frequency field. For each frequency, we keep track of the elements and move them around when accessed. Get would be O(1), Put could potentially be worst case unbounded (we have to loop from frequency 0 to integer.max_value), or could be optimized to O(N)
Solutions
Using LinkedHashMap
1 // I really don't like inheritance, i prefer composition, but the removeEldestEntry is a protected method, have to do this 2 public class LRULinkedHashMap extends LinkedHashMap<Integer, Integer> { 3 private int maxSize = -1; 4 5 // This is simply use a LinkedHashMap, sort of cheating 6 public LRULinkedHashMap(int capacity) { 7 super(16, 0.75f, true); // the 16 hashtable size is not to be confused with the max cache size 8 this.maxSize = capacity; 9 } 10 11 public int get(int key) { 12 return (int)super.getOrDefault(key, -1); 13 } 14 15 public void put(int key, int value) { 16 super.put(key, value); 17 } 18 19 // as suggested in the java doc 20 @Override 21 public boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { 22 return super.size() > this.maxSize; 23 } 24 }
Time Complexity: O(1) for both get and set
Space Complexity: O(N) Where N is the number of items inserted
Using Doubly LinkedList & Hashmap
1 public class LRUCacheGoodVersion { 2 3 public class Node { 4 public int val; 5 public int key; 6 public Node pre = null; 7 public Node next = null; 8 9 public Node(int k, int v) { 10 this.key = k; 11 this.val = v; 12 } 13 } 14 15 // encapsulates a DoublyList class 16 public class DoublyList { 17 // for doubly linked list, keep two dummies, head and tail 18 Node dummyHead = null; 19 Node dummyTail = null; 20 21 public void addLast(Node node) { 22 node.pre = this.dummyTail.pre; 23 this.dummyTail.pre.next = node; 24 this.dummyTail.pre = node; 25 node.next = this.dummyTail; 26 } 27 28 public Node getHead() { 29 return this.dummyHead.next; 30 } 31 32 public void remove(Node n) { 33 n.pre.next = n.next; 34 n.next.pre = n.pre; 35 n.pre = null; 36 n.next = null; 37 } 38 39 public DoublyList() { 40 this.dummyHead = new Node(Integer.MIN_VALUE, -1); 41 this.dummyTail = new Node(Integer.MIN_VALUE, -1); 42 43 this.dummyHead.next = this.dummyTail; 44 this.dummyTail.pre = this.dummyHead; // head <-> last, this is what i like about two dummy nodes 45 } 46 } 47 48 private int capacity; 49 private Map<Integer, Node> lookup = null; 50 private DoublyList list = null; 51 52 public LRUCacheGoodVersion(int capacity) { 53 this.capacity = capacity; 54 this.lookup = new HashMap<Integer, Node>(); 55 this.list = new DoublyList(); 56 } 57 58 public int get(int key) { 59 if (this.lookup.containsKey(key)) { 60 Node n = this.lookup.get(key); 61 this.promoteToLast(n); 62 return n.val; 63 } else { 64 return -1; 65 } 66 } 67 68 public void set(int key, int value) { 69 if (this.lookup.containsKey(key)) { 70 Node n = this.lookup.get(key); 71 n.val = value; 72 this.promoteToLast(n); 73 return; 74 } 75 76 if (this.lookup.size() == this.capacity) { 77 Node leastUsed = this.list.getHead(); 78 this.list.remove(leastUsed); 79 this.lookup.remove(leastUsed.key); 80 } 81 82 Node n = new Node(key, value); 83 this.lookup.put(key, n); 84 this.list.addLast(n); 85 } 86 87 private void promoteToLast(Node n) { 88 this.list.remove(n); 89 this.list.addLast(n); 90 } 91 }
Time Complexity: O(1) for both get and set
Space Complexity: O(N) Where N is the number of items inserted