前言:java
【小王的困惑】node
首先考慮這樣的一個業務場景,小王在A公司上班,有一天產品提出了一個需求:「我們系統的用戶啊,天天活躍的就那麼多,有太多的殭屍用戶,根本不登陸,你能不能考慮作一個篩選機制把這些用戶刨出去,而且給活躍的用戶作一個排名,咱們能夠設計出一些獎勵活動,提高我們的用戶粘性,我們只須要關注那些活躍的用戶就好了「」。小王連忙點頭,說能夠啊,然而內心犯起嘀咕來了:這簡直,按照常規思路,給用戶添加一個最近活躍時間長度和登陸次數,而後按照這兩個數據計算他們的活躍度,最後直接排序就好了。嘿嘿,簡直完美!不過!用戶表字段已經不少了,又要加兩個字段,而後還得遍歷全部的數據排序?這樣查詢效率是否是會受影響啊?而且公司的服務器上次就蹦過一次,差點沒忙出命來才調好。有沒有更優雅的一種方式呢?小王面朝天空45°,陷入了無限的思考中.....算法
本篇博客的目錄緩存
一:LRU是什麼?服務器
二:LRU的實現數據結構
三:測試app
四:總結ide
一:LRU是什麼?性能
1.1:LRU是什麼?按照英文的直接原義就是Least Recently Used,最近最久未使用法,它是按照一個很是註明的計算機操做系統基礎理論得來的:最近使用的頁面數據會在將來一段時期內仍然被使用,已經好久沒有使用的頁面頗有可能在將來較長的一段時間內仍然不會被使用。基於這個思想,會存在一種緩存淘汰機制,每次從內存中找到最久未使用的數據而後置換出來,從而存入新的數據!它的主要衡量指標是使用的時間,附加指標是使用的次數。在計算機中大量使用了這個機制,它的合理性在於優先篩選熱點數據,所謂熱點數據,就是最近最多使用的數據!由於,利用LRU咱們能夠解決不少實際開發中的問題,而且很符合業務場景。測試
1.2:小王的困惑
當小王看到LRU的時候,瞬間感受抓住了救命稻草,這個算法不是就徹底契合產品的需求嗎?只要把用戶數據按照LRU去篩選,利用數據結構完成的事情,徹底減小了本身存儲、添加字段判斷、排序的過程,這樣對於提升服務器性能確定有很大的幫助,豈不美哉!小王考慮好以後,就決定先寫一個demo來實現LRU,那麼在java中是如何實現LRU呢?考慮了許久,小王寫下了這些代碼。
二:LRU的實現
2.1:利用雙向鏈表實現
雙向鏈表有一個特色就是它的鏈表是雙路的,咱們定義好頭節點和尾節點,而後利用先進後出(FIFO),最近被放入的數據會最先被獲取。其中主要涉及到添加、訪問、修改、刪除操做。首先是添加,若是是新元素,直接放在鏈表頭上面,其餘的元素順序往下移動;訪問的話,在頭節點的能夠不用管,若是是在中間位置或者尾巴,就要將數據移動到頭節點;修改操做也同樣,修改原值以後,再將數據移動到頭部;刪除的話,直接刪除,其餘元素順序移動;
2.2:java實現的代碼
2.2.1:定義基本的鏈表操做節點
public class Node { //鍵 Object key; //值 Object value; //上一個節點 Node pre; //下一個節點 Node next; public Node(Object key, Object value) { this.key = key; this.value = value; } }
2.2.2:鏈表基本定義
咱們定義一個LRU類,而後定義它的大小、容量、頭節點、尾節點等部分,而後一個基本的構造方法
public class LRU<K, V> { private int currentSize;//當前的大小 private int capcity;//總容量 private HashMap<K, Node> caches;//全部的node節點 private Node first;//頭節點 private Node last;//尾節點 public LRU(int size) { currentSize = 0; this.capcity = size; caches = new HashMap<K, Node>(size); }
2.2.3:添加元素
添加元素的時候首先判斷是否是新的元素,若是是新元素,判斷當前的大小是否是大於總容量了,防止超過總鏈表大小,若是大於的話直接拋棄最後一個節點,而後再以傳入的key\value值建立新的節點。對於已經存在的元素,直接覆蓋舊值,再將該元素移動到頭部,而後保存在map中
/** * 添加元素 * @param key * @param value */ public void put(K key, V value) { Node node = caches.get(key); //若是新元素 if (node == null) { //若是超過元素容納量 if (caches.size() >= capcity) { //移除最後一個節點 caches.remove(last.key); removeLast(); } //建立新節點 node = new Node(key,value); } //已經存在的元素覆蓋舊值 node.value = value; //把元素移動到首部 moveToHead(node); caches.put(key, node); }
2.2.4:訪問元素
經過key值來訪問元素,主要的作法就是先判斷若是是不存在的,直接返回null。若是存在,把數據移動到首部頭節點,而後再返回舊值。
/** * 經過key獲取元素 * @param key * @return */ public Object get(K key) { Node node = caches.get(key); if (node == null) { return null; } //把訪問的節點移動到首部 moveToHead(node); return node.value; }
以下所示,訪問key=3這個節點的時候,須要把3移動到頭部,這樣能保證整個鏈表的頭節點必定是特色數據(最近使用的數據!)
2.2.5:節點刪除操做
在根據key刪除節點的操做中,咱們須要作的是把節點的前一個節點的指針指向當前節點下一個位置,再把當前節點的下一個的節點的上一個指向當前節點的前一個,這麼說有點繞,咱們來畫圖來看:
/** * 根據key移除節點 * @param key * @return */ public Object remove(K key) { Node node = caches.get(key); if (node != null) { if (node.pre != null) { node.pre.next = node.next; } if (node.next != null) { node.next.pre = node.pre; } if (node == first) { first = node.next; } if (node == last) { last = node.pre; } } return caches.remove(key); }
假設如今要刪除3這個元素,咱們第一步要作的就是把3的pre節點4(這裏說的都是key值)的下一個指針指向3的下一個節點2,再把3的下一個節點2的上一個指針指向3的上一個節點4,這樣3就消失了,從4和2之間斷開了,4和2不再須要3來進行鏈接,從而實現刪除的效果。
2.2.6:移動元素到頭節點
首先把當前節點移除,相似於刪除的效果(可是沒有移除該元素),而後再將首節點設爲當前節點的下一個,再把當前節點設爲頭節點的前一個節點。當前幾點設爲首節點。再把首節點的前一個節點設爲null,這樣就是間接替換了頭節點爲當前節點。
/** * 把當前節點移動到首部 * @param node */ private void moveToHead(Node node) { if (first == node) { return; } if (node.next != null) { node.next.pre = node.pre; } if (node.pre != null) { node.pre.next = node.next; } if (node == last) { last = last.pre; } if (first == null || last == null) { first = last = node; return; } node.next = first; first.pre = node; first = node; first.pre = null; }
三:測試
代碼寫完了,咱們來測試一下結果:
public static void main(String[] args) { LRU<Integer, String> lru = new LRU<Integer, String>(5); lru.put(1, "a"); lru.put(2, "b"); lru.put(3, "c"); lru.put(4,"d"); lru.put(5,"e"); System.out.println("原始鏈表爲:"+lru.toString()); lru.get(4); System.out.println("獲取key爲4的元素以後的鏈表:"+lru.toString()); lru.put(6,"f"); System.out.println("新添加一個key爲6以後的鏈表:"+lru.toString()); lru.remove(3); System.out.println("移除key=3的以後的鏈表"+lru.toString()); }
首先咱們先新放入幾個元素,而後再嘗試訪問第4個節點,再放入一個元素,再移除一個元素,看看會輸出多少:
原始鏈表爲:5:e 4:d 3:c 2:b 1:a 獲取key爲4的元素以後的鏈表:4:d 5:e 3:c 2:b 1:a 新添加一個key爲6以後的鏈表:6:f 4:d 5:e 3:c 2:b 移除key=3的以後的鏈表:6:f 4:d 5:e 2:b
看結果發現和咱們預期的一致,不管添加和獲取元素以後整個鏈表都會將最近訪問的元素移動到訂點,這樣保證了咱們每次取到的最熱點的數據,這就是LRU的最重要思想.
四:總結
本篇博客主要講述了LRU的算法實現,理解了LRU也能幫助咱們理解LinkedList,由於linkedList自己就是雙向鏈表。還有就是理解數據結構這種方式,以及LRU的移動節點的過程,若是能在實際的開發中利用它的特性使用到合適的業務場景中。
附加:java實現LRU的完整代碼:
import java.util.HashMap; public class LRU<K, V> { private int currentSize;//當前的大小 private int capcity;//總容量 private HashMap<K, Node> caches;//全部的node節點 private Node first;//頭節點 private Node last;//尾節點 public LRU(int size) { currentSize = 0; this.capcity = size; caches = new HashMap<K, Node>(size); } /** * 放入元素 * @param key * @param value */ public void put(K key, V value) { Node node = caches.get(key); //若是新元素 if (node == null) { //若是超過元素容納量 if (caches.size() >= capcity) { //移除最後一個節點 caches.remove(last.key); removeLast(); } //建立新節點 node = new Node(key,value); } //已經存在的元素覆蓋舊值 node.value = value; //把元素移動到首部 moveToHead(node); caches.put(key, node); } /** * 經過key獲取元素 * @param key * @return */ public Object get(K key) { Node node = caches.get(key); if (node == null) { return null; } //把訪問的節點移動到首部 moveToHead(node); return node.value; } /** * 根據key移除節點 * @param key * @return */ public Object remove(K key) { Node node = caches.get(key); if (node != null) { if (node.pre != null) { node.pre.next = node.next; } if (node.next != null) { node.next.pre = node.pre; } if (node == first) { first = node.next; } if (node == last) { last = node.pre; } } return caches.remove(key); } /** * 清除全部節點 */ public void clear() { first = null; last = null; caches.clear(); } /** * 把當前節點移動到首部 * @param node */ private void moveToHead(Node node) { if (first == node) { return; } if (node.next != null) { node.next.pre = node.pre; } if (node.pre != null) { node.pre.next = node.next; } if (node == last) { last = last.pre; } if (first == null || last == null) { first = last = node; return; } node.next = first; first.pre = node; first = node; first.pre = null; } /** * 移除最後一個節點 */ private void removeLast() { if (last != null) { last = last.pre; if (last == null) { first = null; } else { last.next = null; } } } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node node = first; while (node != null) { sb.append(String.format("%s:%s ", node.key, node.value)); node = node.next; } return sb.toString(); } public static void main(String[] args) { LRU<Integer, String> lru = new LRU<Integer, String>(5); lru.put(1, "a"); lru.put(2, "b"); lru.put(3, "c"); lru.put(4,"d"); lru.put(5,"e"); System.out.println("原始鏈表爲:"+lru.toString()); lru.get(4); System.out.println("獲取key爲4的元素以後的鏈表:"+lru.toString()); lru.put(6,"f"); System.out.println("新添加一個key爲6以後的鏈表:"+lru.toString()); lru.remove(3); System.out.println("移除key=3的以後的鏈表:"+lru.toString()); } }