LRU (最近最少使用頁面置換演算法) 的程式碼實現
阿新 • • 發佈:2021-04-30
閱讀之前推薦先閱讀博主關於LinkedHashMap的文章,傳送地址:LinkedHashMap原始碼分析,基於JDK1.8逐行分析
LRU演算法的實現
文章目錄
1. 題目描述
運用你所掌握的資料結構,設計和實現一個 LRU (最近最少使用) 快取機制
實現 LRUmap 類:
- LRUmap(int capacity) 以正整數作為容量 capacity 初始化 LRU 快取
- int get(int key) 如果關鍵字 key 存在於快取中,則返回關鍵字的值,否則返回 -1
- void put(int key, int value) 如果關鍵字已經存在,則變更其資料值;如果關鍵字不存在,則插入該組「關鍵字-值」。當快取容量達到上限capacity時,應該在寫入新資料之前刪除最久未使用的資料值,從而為新的資料值留出空間
進階:是否可以在 O(1) 時間複雜度內完成這兩種操作?
2. LRU演算法的介紹
-
全稱為最近最少使用,是一種快取淘汰策略,也就是說認為最近使用過的資料應該是有用的,很久都沒使用過的資料應該是無用的,記憶體滿了就優先刪除那些很久沒有使用過的資料
-
對應到資料結構表述的就是最常使用的元素將其移動至頭部或尾部,這就會導致很久沒有使用過的元素會被動的移動至另一邊,最先刪除的元素是最久沒有被使用過的元素(注意不是使用次數最少的元素,這是LFU演算法)
- get一個元素時算是訪問該元素,需要將此元素移動至頭部或尾部,表示最近使用過
- put一個元素時算是訪問該元素,需要將此元素插入到頭部或尾部,表示最近使用過
3. 資料結構的選擇
3.1 為什麼不使用陣列?
陣列查詢一個元素的時間複雜度為O(1),但其刪除元素後將元素整體移動的時間複雜度為O(n),不滿足題意
3.2 為什麼不使用單向連結串列?
連結串列 新增 / 刪除 元素的時間複雜度為O(1),如果刪除的是頭節點則滿足題意
但如果刪除的是連結串列的中間節點,需要儲存待刪除節點的前一個節點,且遍歷到待刪除節點的時間複雜度為O(n),不滿足題意
3.3 為什麼使用HashMap + 雙向連結串列?
HashMap尋找待刪除節點只需要O(1)的時間複雜度
雙向連結串列刪除節點不需要遍歷找到待刪除結點的前一個節點,故刪除任意位置的元素時間複雜度都是O(1)
綜上所述:使用HashMap尋找節點,使用雙向連結串列 刪除 / 移動 節點,可滿足時間複雜度為O(1)的要求
整體結構如下圖所示:
4. 程式碼實現方式一
將剛剛使用過的元素移動到頭部,很久沒有使用過的元素移動到尾部
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> map = 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;
}
//get方法
public int get(int key) {
DLinkedNode node = map.get(key);
if (node == null) {
return -1;
}
//如果key存在,先通過HashMap定位,再移到頭部
moveToHead(node);
return node.value;
}
//put方法
public void put(int key, int value) {
DLinkedNode node = map.get(key);
if (node == null) {
//如果key不存在,建立一個新的節點
DLinkedNode newNode = new DLinkedNode(key, value);
//新增進雜湊表
map.put(key, newNode);
//新增至雙向連結串列的頭部
addToHead(newNode);
++size;
if (size > capacity) {
//如果元素個數超出容量,刪除雙向連結串列的尾節點
DLinkedNode tail = removeTail();
//刪除雜湊表中對應的項
map.remove(tail.key);
--size;
}
}
else {
//如果key存在,先通過HashMap定位,再修改value並移到頭部
node.value = value;
moveToHead(node);
}
}
//將節點移動至頭部
private void moveToHead(DLinkedNode node) {
removeNode(node); //將此節點從連結串列斷開
addToHead(node); //將斷開的節點移動至頭部
}
//將節點從連結串列斷開
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
//將節點移動至頭部
private void addToHead(DLinkedNode node) {
node.prev = head; //偽頭部節點成為此節點的前一個節點
node.next = head.next; //原來的第一個節點成為此節點的後一個節點
head.next.prev = node; //原來的第一個節點向前的指標指向此節點
head.next = node; //此節點成為偽頭部節點的後一個節點
}
//刪除連結串列的尾節點
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev; //刪除的其實是偽尾部的前一個節點
removeNode(res);
return res;
}
}
5. 程式碼實現方式二
通過LinkedHashMap的原始碼講解,自定義LRU快取可以使用現有的LinkedHashMap,通過制定移除策略、呼叫 getOrDefault
方法未找到元素時返回 -1 即可,程式碼如下:
class LRUCache extends LinkedHashMap<Integer, Integer>{
//容量
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1); //未找到元素時返回-1
}
//自帶的put方法可滿足要求,無需改寫
public void put(int key, int value) {
super.put(key, value);
}
//制定移除策略
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}