題目及測試
package pid146;
/* LRU緩存機制
運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持以下操作: 獲取數據 get 和 寫入數據 put 。
獲取數據 get(key) - 如果關鍵字 (key) 存在於緩存中,則獲取關鍵字的值(總是正數),否則返回 -1。
寫入數據 put(key, value) - 如果關鍵字已經存在,則變更其數據值;如果關鍵字不存在,則插入該組「關鍵字/值」。
當緩存容量達到上限時,它應該在寫入新數據之前刪除最久未使用的數據值,從而爲新的數據值留出空間。
進階:
你是否可以在 O(1) 時間複雜度內完成這兩種操作?
示例:
LRUCache cache = new LRUCache( 2 緩存容量 );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 該操作會使得關鍵字 2 作廢
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 該操作會使得關鍵字 1 作廢
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
*/
public class main {
public static void main(String[] args) {
LRUCache cache = new LRUCache( 2 );
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 返回 1
cache.put(3, 3); // 該操作會使得關鍵字 2 作廢
System.out.println(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 該操作會使得關鍵字 1 作廢
System.out.println(cache.get(1)); // 返回 -1 (未找到)
System.out.println(cache.get(3)); // 返回 3
System.out.println(cache.get(4)); // 返回 4
}
}
解法1(成功,181ms,很慢)
通過hashmap進行key和value的存取與讀取的操作
通過linkedList雙端隊列,完成LRU的操作,尾部是最近的操作,頭部是最遠的操作,get時,會先搜索key,然後把key放到隊尾,這步是O(N),put時,如果key已經在隊列,如果get一樣,也是O(N)
由於在雙端隊列搜索一個節點的速度很慢,都是O(N),所以整體速度也是O(N)
package pid146;
import java.util.HashMap;
import java.util.LinkedList;
public class LRUCache {
int capacity = 0;
int size = 0;
HashMap<Integer, Integer> mapValue = new HashMap<>();
LinkedList<Integer> linkedList= new LinkedList<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if(mapValue.containsKey(key)){
linkedList.remove(new Integer(key));
linkedList.addLast(key);
return mapValue.get(key);
}else{
return -1;
}
}
public void put(int key, int value) {
if(mapValue.containsKey(key)){
linkedList.remove(new Integer(key));
linkedList.addLast(key);
}else{
linkedList.addLast(key);
size++;
}
if(size>capacity){
int removed = linkedList.removeFirst();
mapValue.remove(removed);
size--;
}
mapValue.put(key, value);
}
}
/**
* 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);
*/
解法2(別人的)
類似於解法1,就是map裏的value變成節點,導致搜索節點的操作變爲O(1)
LRU 緩存機制可以通過哈希表輔以雙向鏈表實現,我們用一個哈希表和一個雙向鏈表維護所有在緩存中的鍵值對。
雙向鏈表按照被使用的順序存儲了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。
哈希表即爲普通的哈希映射(HashMap),通過緩存數據的鍵映射到其在雙向鏈表中的位置。
這樣以來,我們首先使用哈希表進行定位,找出緩存項在雙向鏈表中的位置,隨後將其移動到雙向鏈表的頭部,即可在 O(1)的時間內完成 get 或者 put 操作。具體的方法如下:
對於 get 操作,首先判斷 key 是否存在:
如果 key 不存在,則返回 −1;
如果 key 存在,則 key 對應的節點是最近被使用的節點。通過哈希表定位到該節點在雙向鏈表中的位置,並將其移動到雙向鏈表的頭部,最後返回該節點的值。
對於 put 操作,首先判斷 key 是否存在:
如果 key 不存在,使用 key 和 value 創建一個新的節點,在雙向鏈表的頭部添加該節點,並將 key 和該節點添加進哈希表中。然後判斷雙向鏈表的節點數是否超出容量,如果超出容量,則刪除雙向鏈表的尾部節點,並刪除哈希表中對應的項;
如果 key 存在,則與 get 操作類似,先通過哈希表定位,再將對應的節點的值更新爲 value,並將該節點移到雙向鏈表的頭部。
上述各項操作中,訪問哈希表的時間複雜度爲 O(1),在雙向鏈表的頭部添加節點、在雙向鏈表的尾部刪除節點的複雜度也爲 O(1)。而將一個節點移到雙向鏈表的頭部,可以分成「刪除該節點」和「在雙向鏈表的頭部添加節點」兩步操作,都可以在 O(1)時間內完成。
在雙向鏈表的實現中,使用一個僞頭部(dummy head)和僞尾部(dummy tail)標記界限,這樣在添加節點和刪除節點的時候就不需要檢查相鄰的節點是否存在。
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用僞頭部和僞尾部節點
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通過哈希表定位,再移到頭部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,創建一個新的節點
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加進哈希表
cache.put(key, newNode);
// 添加至雙向鏈表的頭部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,刪除雙向鏈表的尾部節點
DLinkedNode tail = removeTail();
// 刪除哈希表中對應的項
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通過哈希表定位,再修改 value,並移到頭部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}