1. 程式人生 > 其它 >LRU (最近最少使用頁面置換演算法) 的程式碼實現

LRU (最近最少使用頁面置換演算法) 的程式碼實現

閱讀之前推薦先閱讀博主關於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)的要求

整體結構如下圖所示:

image-20210419215308544

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; 
    }
}