一、題目描述
二、分析題目
2.1 LRU 算法介紹
計算機的緩存容量有限,如果緩存滿了就要刪除一些內容,給新內容騰位置。但問題是,刪除哪些內容呢?我們肯定希望刪掉哪些沒什麼用的緩存,而把有用的數據繼續留在緩存裏,方便之後繼續使用。那麼,什麼樣的數據,我們判定爲有用的的數據呢?
LRU 緩存淘汰算法就是一種常用策略。LRU 的全稱是 Least Recently Used,也就是說我們認爲最近使用過的數據應該是有用的,很久都沒用過的數據應該是無用的,內存滿了就優先刪那些很久沒用過的數據。
2.2 LRU 算法描述
LRU 算法實際上是讓你設計數據結構:首先要接收一個 capacity 參數作爲緩存的最大容量,然後實現兩個 API,一個是 put(key, val) 方法存入鍵值對,另一個是 get(key) 方法獲取 key 對應的 val,如果 key 不存在則返回 -1。
注意哦,get 和 put 方法必須都是 O(1) 的時間複雜度,我們舉個具體例子來看看 LRU 算法怎麼工作。
/* 緩存容量爲 2 */
LRUCache cache = new LRUCache(2);
// 你可以把 cache 理解成一個隊列
// 假設左邊是隊頭,右邊是隊尾
// 最近使用的排在隊頭,久未使用的排在隊尾
// 圓括號表示鍵值對 (key, val)
cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
cache.get(1); // 返回 1
// cache = [(1, 1), (2, 2)]
// 解釋:因爲最近訪問了鍵 1,所以提前至隊頭
// 返回鍵 1 對應的值 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// 解釋:緩存容量已滿,需要刪除內容空出位置
// 優先刪除久未使用的數據,也就是隊尾的數據
// 然後把新的數據插入隊頭
cache.get(2); // 返回 -1 (未找到)
// cache = [(3, 3), (1, 1)]
// 解釋:cache 中不存在鍵爲 2 的數據
cache.put(1, 4);
// cache = [(1, 4), (3, 3)]
// 解釋:鍵 1 已存在,把原始值 1 覆蓋爲 4
// 不要忘了也要將鍵值對提前到隊頭
2.3 LRU 算法設計
三、編寫代碼
3.1 編寫雙鏈表節點類 Node
package question6;
/**
* @description: 雙鏈表的節點類
* @author: hyr
* @time: 2020/5/16 16:44
*/
public class Node {
public int key, val;
public Node next, prev;
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
3.2 編寫雙鏈表類 DoubleList
package question6;
/**
* @description: 雙鏈表類
* @author: hyr
* @time: 2020/5/16 16:46
*/
public class DoubleList {
private Node head, tail; // 頭尾虛節點
private int size; // 鏈表元素數
public DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 在鏈表頭部添加節點 x
public void addFirst(Node x) {
x.next = head.next;
x.prev = head;
head.next.prev = x;
head.next = x;
size++;
}
// 刪除鏈表中的 x 節點(x 一定存在)
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 刪除鏈表中最後一個節點,並返回節點
public Node removeLast() {
if (tail.prev == head) {
return null;
}
Node last = tail.prev;
remove(last);
return last;
}
// 返回鏈表長度
public int size() {
return size;
}
}
到這裏就能回答剛纔“爲什麼必須要用雙向鏈表”的問題了,因爲我們需要刪除操作。刪除一個節點不光要得到該節點本身的指針,也需要操作其前驅節點的指針,而雙向鏈表才能支持直接查找前驅,保證操作的時間複雜度 O(1)。
3.3 編寫 LRUCache 類
有了雙向鏈表的實現,我們只需要在 LRU 算法中把它和哈希表結合起來即可。
package question6;
import java.util.HashMap;
/**
* @description: LRU 緩存
* @author: hyr
* @time: 2020/5/16 21:09
*/
public class LRUCache {
// key -> Node(key, val)
private HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
private DoubleList cache;
// 最大容量
private int cap;
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
public int get(int key) {
if (!map.containsKey(key)) {
return -1;
}
int val = map.get(key).val;
// 利用 put 方法把該數據提前
put(key, val);
return val;
}
public void put(int key, int val){
// 先把新節點 x 做出來
Node x = new Node(key, val);
if (map.containsKey(key)){
// 刪除舊的節點,新的插入到頭部
cache.remove(map.get(key));
cache.addFirst(x);
// 更新 map 中的數據
map.put(key, x);
} else {
if (cap == cache.size()){
// 刪除鏈表最後一個數據
Node last = cache.removeLast();
map.remove(last.key);
}
// 直接添加到頭部
cache.addFirst(x);
map.put(key,x);
}
}
}
注:文章參考自:https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/