LeetCode算法 -- LRU緩存機制(第11題)

一、題目描述

在這裏插入圖片描述

二、分析題目

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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章