前言:
【小王的困惑】
首先考慮這樣的一個業務場景,小王在A公司上班,有一天產品提出了一個需求:“咱們系統的用戶啊,每天活躍的就那么多,有太多的僵屍用戶,根本不登錄,你能不能考慮做一個篩選機制把這些用戶刨出去,並且給活躍的用戶做一個排名,我們可以設計出一些獎勵活動,提升咱們的用戶粘性,咱們只需要關注那些活躍的用戶就行了“”。小王連忙點頭,說可以啊,然而心里犯起嘀咕來了:這簡單,按照常規思路,給用戶添加一個最近活躍時間長度和登錄次數,然后按照這兩個數據計算他們的活躍度,最后直接排序就行了。嘿嘿,簡直完美!不過!用戶表字段已經很多了,又要加兩個字段,然后還得遍歷所有的數據排序?這樣查詢效率是不是會受影響啊?並且公司的服務器上次就蹦過一次,差點沒忙出命來才調好。有沒有更優雅的一種方式呢?小王面朝天空45°,陷入了無限的思考中.....
本篇博客的目錄
一:LRU是什么?
二:LRU的實現
三:測試
四:總結
一: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); caches.put(key, node); currentSize++; }else { //已經存在的元素覆蓋舊值 node.value = value; } //把元素移動到首部 moveToHead(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()); } }