LRU緩存


leetcode題目-16.25.LRU緩存

設計和構建一個“最近最少使用”緩存,該緩存會刪除最近最少使用的項目。緩存應該從鍵映射到值(允許你插入和檢索特定鍵對應的值),並在初始化時指定最大容量。當緩存被填滿時,它應該刪除最近最少使用的項目。

它應該支持以下操作: 獲取數據 get 和 寫入數據 put 。

獲取數據 get(key) - 如果密鑰 (key) 存在於緩存中,則獲取密鑰的值(總是正數),否則返回 -1。
寫入數據 put(key, value) - 如果密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據之前刪除最近最少使用的數據值,從而為新的數據值留出空間。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/lru-cache-lcci
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。

即如果一組數字,最近使用的放在最左邊,最近不用的放在最右邊。因此如果新寫入一個數字,如果內存滿了,就把最右邊的數字替換掉,新來的數字放在最左邊。如果新獲取一個數據,那么這個數據就是最新使用的了,就更新它的位置。

因此這組數據需要頻繁地換位置,肯定是要使用鏈表的。

//LinkedHashMap實現
class
LRUCache { int capacity; Map<Integer, Integer> map; public LRUCache(int capacity) { this.capacity = capacity; map = new LinkedHashMap<>(); } public int get(int key) { //若密鑰不存在緩存中,則返回-1 if(!map.containsKey(key)){ return -1; } //如果密鑰存在緩存中,則獲取密鑰的值 Integer value = map.remove(key); map.put(key, value); return value; } public void put(int key, int value) { //如果密鑰存在,刪出原數值.即更新該數值的位置 if(map.containsKey(key)){ map.remove(key); map.put(key, value); return; } //如果密鑰不存在,寫入其數據值 map.put(key, value); //如果緩存容量達到上限,那么刪出最近最少使用的數據 //利用迭代器,刪出第一個 if(map.size()>capacity){
//map.entrySet():把HashMap類型的數據轉換為集合類型,獲取鍵值對的集合
       //iterator():獲取這個集合的迭代器 map.remove(map.entrySet().iterator().next().getKey()); } } }
//雙向鏈表+HashMap
public
class LRUCache{ //定義雙向鏈表節點 private class ListNode{ int key; int value; ListNode pre; ListNode next; public ListNode(int key, int value){ this.key = key; this.value = value; pre = null; next = null; } } private int capacity; private Map<Integer, ListNode> map; //虛擬頭節點 private ListNode head; //虛擬尾節點 private ListNode tail; //初始化 public LRUCache(int capacity){ this.capacity = capacity; map = new HashMap<>(); head = new ListNode(-1, -1); tail = new ListNode(-1, -1); //建立虛擬頭節點和尾節點的關系 head.next = tail; tail.pre = head; } public int get(int key){ //若密鑰不存在緩存中,則返回-1 if(!map.containsKey(key)){ return -1; } //如果密鑰存在緩存中,則獲取密鑰的值 ListNode node = map.get(key); //更新密鑰對應值的位置到尾部.
//現在原位置刪除當前節點
node.pre.next = node.next; node.next.pre = node.pre;
//把當前節點加在尾部 moveToTail(node);
return node.value; } public void put(int key, int value){ //如果密鑰存在,刪出原數值.即更新該數值的位置 if(get(key) != -1){ map.get(key).value = value; return; } //如果密鑰不存在,寫入其數據值 ListNode node = new ListNode(key, value); map.put(key, node); moveToTail(node); //如果緩存容量達到上限,那么刪出最近最少使用的數據。刪除頭(虛擬頭節點后面的節點) if(map.size() > capacity){ map.remove(head.next.key); head.next = head.next.next; head.next.pre = head; } } //將節點移動到鏈表的末尾,虛擬尾節點的前面 private void moveToTail(ListNode node){ node.pre = tail.pre; tail.pre = node; node.pre.next = node; node.next = tail; } }

 

帶過期時間功能:

添加一個過期時間隊列&一個過期清除線程,清除的時候使用while(true)判斷隊列隊首位置是否過期

為每個節點放一個過期時間,只要到了這個時間就直接刪除。只要啟動LRU,就開始清除

public class LRU{
      //設置清除過期數據的線程池
      private static ScheduleExecutorService swapExpiredPool = new ScheduledThreadPoolExecutor(10);
      //用戶存儲數據,ConcurrentHashMap用於保證線程安全
private ConcurrentHashMap<String, Node> cache = new ConcurrentHashMap<>(1024);
//保存最新的過期數據,過期時間最小的排在隊列前 private PriorityQueue<Node> expireQueue = new PriorityQueue<>(1024); //構造方法。只要啟動了這個LRU,過期清除線程就開始工作 public LRU(){ swapExpiredPool.scheduleWithFixedDelay(new ExpiredNode(), 3, 3,TimeUnit.SECONDS); } //....... }

ExpireNode當做一個內部類在LRU中

public class ExpiredNode implements Runnable{
       public void run(){
              //獲取當前時間
              long now = System.currentTimeMillis();
              while(true){
                     //從過期隊列彈出隊首元素
                     Node node = expireQueue.peek();
                     //如果不存在或者不過期,就返回
                     if(node==null || node.expireTime>now)
                              return;
                     //如果過期,就從隊列里彈出
                     cache.remove(node.key);
                     expireQueue.poll();   
              }
       }  
}                                                                    

 那么相應的set方法也要有改變,因為要考慮過期時間。Node節點里多一個ExpireTime的字段

    public void put(int key, int value, long ttl){
        //如果密鑰存在,刪出原數值.即更新該數值的位置
        if(get(key) != -1){
            map.get(key).value = value;
            return;
        }
        //獲取過期時間點
        long expireTime = System.currentTimeMillis()+ttl;
        //如果密鑰不存在,寫入其數據值
        ListNode node = new ListNode(key, value, expireTime);
//cache中有的話就覆蓋,沒有的話添加新的。&過期時間隊列也要添加 ListNode old
= cache.put(key, node); expireQueue.add(node); //如果該key存在數據,要從過期時間隊列里刪除 if(old!=null){ expireQueue.remove(old); return old.value; } return null; }

 

 

擴展:

LRU應用場景:

日常開發中,UI界面加載圖片不可能每次都從網絡上下載然后顯示,因此Android提供了LruCache類,用於圖片的內存緩存

A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
一個包含有限數量強引用(平常使用的對象引用方式)的緩存,每次訪問一個值,它都會被移動到隊列的頭部,將一個新的值添加到已經滿了的緩存隊列時,該隊列末尾的值將會被逐出,並且可能會被垃圾回收機制進行回收。

內部實現是通過LinkedHashMap維護一個緩存對象列表。參數分別為初始容量、加載因子、訪問順序(為true即集合的元素順序是訪問順序,訪問后會將該元素放到集合的最后面;為false即按照插入順序)。

初始容量的設置:如初始大小小於1,那么map大小默認為1;否則不斷*2直到大於設置的初始容量。

總緩存大小一般為可用內存的1/8

另一種采用LRU算法的緩存為DisLruCahce,用於實現硬盤緩存。

Java的集合:

  • Collection接口
    • set接口(集):唯一,無序。實現類都線程不安全
      • HashSet:底層是一個數組,適用於少量數據的插入操作
        • LinkedHashSet:繼承了HashSet類,為了保持數據的先后添加順序,又加了鏈表結構,但是效率低。若某個集合需要保證元素不重復&記錄元素的添加順序
      • TreeSet:也實現了SortSet接口,底層紅黑樹,只能存儲相同類型對象的引用
    • list接口(列表):可重復,順序與插入順序一致
      • ArrayList:底層為數組結構,查詢快,增刪改慢
      • Vector:數組。比於ArrayList,由於每個方法都加上了synchronized,因此線程安全&效率低於ArrayList。由於增長率是目前數組長度的100%,ArrayList為50%,因此Vector適合存儲數據量比較大的數據。
      • LinkedList:底層為鏈表結構,查詢慢,增刪改快
  • Map接口:鍵唯一,值不一定唯一
    • HashMap:無序
      • LinkedHashMap:HashMap+LinkedList。對讀取順序有嚴格要求時使用,繼承HashMap,實現了Map接口。桶的鏈表是雙向鏈表,並且可以控制存儲順序。“HashMap桶的鏈表產生是因為產生hash碰撞,所有數據形成鏈表 (紅黑樹) 存儲在一個桶中,LinkedHashMap 中雙向鏈表會串聯所有的數據,也就是說有桶中的數據都是會被這個雙向鏈表管理。”即桶里的鏈表也要實現雙向鏈表的功能(圖源於:https://www.cnblogs.com/xiaoxi/p/6170590.html)
    • TreeMap:基於紅黑樹實現,根據鍵的自然順序進行排序
    • HashTable:無序,任何非空的對象都可作為key/value,線程安全

迭代器iterator

Java采用迭代器為各種容器提供公共的操作接口,使得對容器的遍歷操作與具體的底層實現相隔離。

“Collection集合元素的通用獲取方式:在取元素之前先要判斷集合中有沒有元素,如果有,就把這個元素取出來,繼續在判斷,如果還有就再取出出來。一直把集合中的所有元素全部取出。這種取出方式專業術語稱為迭代。”

因此迭代器要實現兩個方法:

hasNext():仍有元素可迭代,返回true;

next():返回迭代的下一個元素。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM