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接口,底層紅黑樹,只能存儲相同類型對象的引用
- HashSet:底層是一個數組,適用於少量數據的插入操作
- list接口(列表):可重復,順序與插入順序一致
- ArrayList:底層為數組結構,查詢快,增刪改慢
- Vector:數組。比於ArrayList,由於每個方法都加上了synchronized,因此線程安全&效率低於ArrayList。由於增長率是目前數組長度的100%,ArrayList為50%,因此Vector適合存儲數據量比較大的數據。
- LinkedList:底層為鏈表結構,查詢慢,增刪改快
- set接口(集):唯一,無序。實現類都線程不安全
- Map接口:鍵唯一,值不一定唯一
- HashMap:無序
- LinkedHashMap:HashMap+LinkedList。對讀取順序有嚴格要求時使用,繼承HashMap,實現了Map接口。桶的鏈表是雙向鏈表,並且可以控制存儲順序。“HashMap桶的鏈表產生是因為產生hash碰撞,所有數據形成鏈表 (紅黑樹) 存儲在一個桶中,LinkedHashMap 中雙向鏈表會串聯所有的數據,也就是說有桶中的數據都是會被這個雙向鏈表管理。”即桶里的鏈表也要實現雙向鏈表的功能
(圖源於:https://www.cnblogs.com/xiaoxi/p/6170590.html)
- LinkedHashMap:HashMap+LinkedList。對讀取順序有嚴格要求時使用,繼承HashMap,實現了Map接口。桶的鏈表是雙向鏈表,並且可以控制存儲順序。“HashMap桶的鏈表產生是因為產生hash碰撞,所有數據形成鏈表 (紅黑樹) 存儲在一個桶中,LinkedHashMap 中雙向鏈表會串聯所有的數據,也就是說有桶中的數據都是會被這個雙向鏈表管理。”即桶里的鏈表也要實現雙向鏈表的功能
- TreeMap:基於紅黑樹實現,根據鍵的自然順序進行排序
- HashTable:無序,任何非空的對象都可作為key/value,線程安全
- HashMap:無序
迭代器iterator
Java采用迭代器為各種容器提供公共的操作接口,使得對容器的遍歷操作與具體的底層實現相隔離。
“Collection集合元素的通用獲取方式:在取元素之前先要判斷集合中有沒有元素,如果有,就把這個元素取出來,繼續在判斷,如果還有就再取出出來。一直把集合中的所有元素全部取出。這種取出方式專業術語稱為迭代。”
因此迭代器要實現兩個方法:
hasNext():仍有元素可迭代,返回true;
next():返回迭代的下一個元素。