146. LRU 緩存
請你設計并實現一個滿足??LRU (最近最少使用) 緩存?約束的數據結構。
實現?
LRUCache
?類:
LRUCache(int capacity)
?以?正整數?作為容量?capacity
?初始化 LRU 緩存int get(int key)
?如果關鍵字?key
?存在于緩存中,則返回關鍵字的值,否則返回?-1
?。void put(int key, int value)
?如果關鍵字?key
?已經存在,則變更其數據值?value
?;如果不存在,則向緩存中插入該組?key-value
?。如果插入操作導致關鍵字數量超過?capacity
?,則應該?逐出?最久未使用的關鍵字。函數?
get
?和?put
?必須以?O(1)
?的平均時間復雜度運行。示例:
輸入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 輸出 [null, null, null, 1, null, -1, null, -1, 3, 4]解釋 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 緩存是 {1=1} lRUCache.put(2, 2); // 緩存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 該操作會使得關鍵字 2 作廢,緩存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 該操作會使得關鍵字 1 作廢,緩存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
- 最多調用?
2 * 105
?次?get
?和?put
解法1:Map + ArrayList (key)
用一個Map來存放key和value,一個ArrayList來存放訪問順序。
當用戶get、put的key存在時,則從ArrayList中找到對應的key刪除,然后把新訪問的key放到尾部。
頭部表示最久未訪問的數據,尾部表示最新訪問的數據。與數據結構匹配。
class LRUCache {private int capacity;private Map<Integer,Integer> cache;private List<Integer> delete;public LRUCache(int capacity) {this.cache = new HashMap<>(capacity);this.capacity = capacity;this.delete = new ArrayList<>(capacity);}public int get(int key) {Integer value = cache.get(key);if(value != null){// update to delete queuedelete.remove((Integer)key);delete.add((Integer)key);return value;}return -1;}public void put(int key, int value) {// if existedif (cache.containsKey(key)) {// update to delete queuedelete.remove((Integer)key);delete.add((Integer)key);cache.put(key, value);return;}// if cache is full, need to deleteif(cache.size() == capacity){Integer deleteKey = delete.remove(0);cache.remove(deleteKey);}cache.put(key, value);delete.add(key);}}/*** Your LRUCache object will be instantiated and called as such:* LRUCache obj = new LRUCache(capacity);* int param_1 = obj.get(key);* obj.put(key,value);*/
?
性能不怎么好,花了883ms。大部分時間花在ArrayList中查找key的位置和刪除key后移動元素上了。
那我們想辦法改進下,查找和刪除。
解法2:Map + ArrayList (key, timestamp)
我們看到解法1中大部分時間都是在ArrayList中查找key和刪除后移動數據。那么我們能不能不移動數據?答案是可以的。
方法是:我們每次操作key后為其引入一個timestamp,需要刪除這個key時,比較待刪除的timestamp是否沒有更新過,沒更新過則刪除。這里使用程序啟動后的納秒,防止在同一個微秒內完成了多個操作,不能區分。
class LRUCache {private int capacity;private Map<Integer,KeyTime> cache;private List<KeyTime> delete;public LRUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>(capacity*3/2);this.delete = new LinkedList<>();}public int get(int key) {KeyTime kt = cache.get(key);if(kt != null){long time = System.nanoTime();kt.time = time;// update to delete queuedelete.add(new KeyTime(key, kt.value, time));return kt.value;}return -1;}public void put(int key, int value) {long time = System.nanoTime();// if existedKeyTime kt = cache.get(key);if (kt != null) {kt.time = time;kt.value = value;// update to delete queuedelete.add(new KeyTime(key, value, time));return;}// if cache is full, need to deleteif(cache.size() == capacity){while(true) {KeyTime deleteKt = delete.remove(0);KeyTime existKt = cache.get(deleteKt.key);if (existKt != null && existKt.time == deleteKt.time) {cache.remove(deleteKt.key);break;}}}cache.put(key, new KeyTime(key, value, time));delete.add(new KeyTime(key, value, time));}static class KeyTime {int key;int value;long time;public KeyTime(int key, int value, long time) {this.key = key;this.value = value;this.time = time;}}}/*** Your LRUCache object will be instantiated and called as such:* LRUCache obj = new LRUCache(capacity);* int param_1 = obj.get(key);* obj.put(key,value);*/
性能已經從883ms提升到了52ms,歷史性的飛躍。還不錯,可惜才28.88%,要是還想進一步優化的話,可以參考官方的LinkedHashMap實現。
?
解法3:HashMap + 雙向鏈表 = LinkedHashMap
這里get/put 后把新訪問的節點移動到表尾處,表頭存放的是最久未訪問的數據。
具體實現如下:
?
public class LRUCache {/*** use double direction linked list to store the order which to evict over due key.*/private int capacity;private Map<Integer,Node> cache;// store the first node to delete which is unused for a long timeprivate Node head;// store the used node recentlyprivate Node tail;public LRUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>(capacity);this.head = new Node(-1, 0, null, null);this.tail = new Node(-2, 0, head, null);this.head.next = tail;}public int get(int key) {Node node = cache.get(key);if (node != null) {// existed then move to headmoveToTail(node);return node.value;}return -1;}public void put(int key, int value) {Node node = cache.get(key);if (node != null) {// existed, only need to update positionmoveToTail(node);node.value = value;return;}// cache is full, get the last unused node to removeif (cache.size() == capacity) {// remove the head node which is unused for a long timeNode toDelete = head.next;deleteNode(toDelete);cache.remove(toDelete.key);}node = new Node(key, value, null, null);cache.put(key, node);addToTail(node);}static class Node {int key;int value;Node previous;Node next;public Node(int key, int value, Node previous, Node next) {this.key = key;this.value = value;this.previous = previous;this.next = next;}}public void moveToTail(Node node) {deleteNode(node);addToTail(node);}/*** delete node from list.*/public void deleteNode(Node node) {node.previous.next = node.next;node.next.previous = node.previous;}/*** add node to list tail.*/public void addToTail(Node node) {node.next = tail;node.previous = tail.previous;tail.previous.next = node;tail.previous = node;}
}
運行時間43ms,超越98.98%,可以了。?