Java〖 LRU緩存機制〗力扣146手撕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

一. 分析 LinkedHashMap

LRU是Least Recently Used的縮寫,即最近最少使用,是一種常用的頁面置換算法,要如何實現呢?

1.1 LinkedHashMap實現

首先看到了LinkedHashMap 底層是一個Entery結點,爲了實現插入快有序和快速訪問兩個優點,結合了HashMap與雙向鏈表
在這裏插入圖片描述
這邊不深入去講,主要來說說LinkedHashMap存儲數據是有序的,而且分爲兩種:插入順序和訪問順序。下圖主要爲訪問順序時結點變更情況

  • 插入順序 表示LinkedHashMap中存儲的順序是按照調用put方法插入的順序進行排序的(默認設置爲: false)
    HashMap中有Entry1、Entry2、Entry3,並設置LinkedHashMap爲訪問順序,則更新Entry1時,會先把Entry1從雙向鏈表中刪除,然後再把Entry1加入到雙向鏈表的表尾,而Entry1在HashMap結構中的存儲位置沒有變化,對比圖如下所示:
    在這裏插入圖片描述

這時候,有些小夥伴就會驚訝的發現,這不就是LRU本U嗎? 但其實LinkedHashMap是會不斷擴容的,要想真正實現LRU,你得去重寫removeEldestEntry()方法

重寫removeEldestEntry方法,當達到條件,就返回ture ,返回true就刪除最近最少使用的Entery結點

 @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest){ //重寫removeEldestEntry方法,當達到條件,就返回ture ,返回true就刪除最近最少使用的Entery結點
        return super.size()>capacity;
    }

1.2 上代碼(LinkedHashMap實現)

/**
 * 繼承LinkedHashMap實現,將構造方法實現成按訪問順序(true),(false)爲按插入順序即爲FIFO
 */
class LRUCache extends LinkedHashMap<Integer,Integer> {
    int capacity;


    public LRUCache(int capacity) {
        super(capacity,075f,true);
        this.capacity=capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key,-1); //這裏找不到就返回-1
    }

    public void put(int key, int value) {
        super.put(key,value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest){ //重寫removeEldestEntry方法,當達到條件,就返回ture ,返回true就刪除最近最少使用的Entery結點
        return super.size()>capacity;
    }
}

繼承LinkedHashMap實現,將構造方法實現成按訪問順序(true),(false)爲按插入順序即爲FIFO

二. 手寫LRU

既然原理都懂了,不如我們自己手寫一個LRU加深印象,不就是一個HashMap跟雙向鏈表嗎,幹他就完了

2.1 分析

雙向鏈表與hashmap結合

  • 對於get操作,判斷是否插已插入

    1. 已插入的話,我們通過hash查到當前結點,更新value,並放到鏈表最頭
    2. 對於沒插入的結點,那我們通過頭插法插入鏈表最前,然後再判斷當前容量是否大於設置容量,大於的話刪除最尾結點
  • 對於put操作,判斷key是否存在

    1. 已存在的話,我們利用hash找到,並放到表頭,返回該結點的值
    2. 不存在的話返回-1即可

2.2 雙向鏈表

class DoubleList{  //雙向鏈表
    int key;
    int value;
    DoubleList pre;
    DoubleList next;

    public DoubleList(int key, int value) {
        this.key = key;
        this.value = value;
    }

    public DoubleList() {
    }
}

2.3 LRU實現類

註釋基本都有,哪裏出錯了評論區見

class LRUCache1 {

    int size; //當前容量
    int capacity; //最大容量
    DoubleList head; //頭指針
    DoubleList tail; //尾指針
    Map<Integer,DoubleList> map=new HashMap<>(); //hash快速查找

    public LRUCache1(int capacity) {
       size=0;
       this.capacity=capacity;
       head=new DoubleList();
       tail=new DoubleList();
       head.next=tail;
       tail.pre=head;
    }

    /**
     * 查找該結點
     * @param key
     * @return
     */
    public int get(int key) {
        DoubleList findVal = map.get(key);
        if(findVal!=null){
            moveToHead(findVal);
            return findVal.value;
        }else return -1;
    }

    /**
     * 新增結點
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        DoubleList findVal = map.get(key); //查詢是否存在該key
        if(findVal==null){ //不存在
            DoubleList doubleList = new DoubleList(key, value); //新建這個結點
            map.put(key,doubleList); //扔到map中去
            addToHead(doubleList); //放到鏈表頭部
            size++; //自增容量
            if(size>capacity) { //若當前容量大於最大容量
                DoubleList removeTail = removeTail(); //找到最後一個結點,刪除
                map.remove(removeTail.key); //在map中刪除該key
                size--;
            }
        }else { //若該結點存在
            findVal.value=value; //更新這個結點的值
            moveToHead(findVal); //刪除並移動到鏈表頭部
        }
    }

    /**
     * 包含兩個操作
     *   1.在鏈表中刪除該結點
     *   2.將該結點加到鏈表頭部
     * @param doubleList
     */
    private void moveToHead(DoubleList doubleList){
        removeNode(doubleList);
        addToHead(doubleList);
    }

    /**
     * 在鏈表中刪除該結點
     * @param doubleList
     */
    private void removeNode(DoubleList doubleList){
       doubleList.pre.next=doubleList.next;
       doubleList.next.pre=doubleList.pre;
    }

    /**
     * 將該結點頭插到鏈表中
     * @param node
     */
    private void addToHead(DoubleList node){
//        node.pre = head;
//        node.next = head.next;
//        head.next.pre = node;
//        head.next = node;

        node.next=head.next;
        node.pre=head;
        head.next.pre=node;
        head.next=node;

    }

    /**
     * 移除隊尾元素
     * @return 返回被刪除的結點(爲了刪除map中的值,所以此處返回結點)
     */
    private DoubleList removeTail() {
        DoubleList temp=tail.pre;
        removeNode(temp);
        return temp;
    }


}

製作不易,轉載請標註~

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