1. 程式人生 > >什麼是LRU演算法?

什麼是LRU演算法?

LRU

一、什麼是LRU?LRU就是Least Recently Used的縮寫,翻譯過來是:最近最少使用,什麼意思呢,請看下面這個示例。

我們要在有限的記憶體中存放一些<K,V>鍵值對,這些鍵值對很多,所有的鍵值對所佔記憶體大於物理可用記憶體,並且每個鍵值對被訪問的情況也是不一樣的。當記憶體用盡的時候,這時新來了一個鍵值對,這時我們要如何處理呢?從記憶體中刪除“潛水”時間最長的那個鍵值對,這樣就可以把新來的鍵值對存入記憶體了,這就是LRU演算法,也是在Redis快取丟棄策略的一種。上面說的“潛水”時間最長指的是:被人遺忘時間最長的那個鍵值對,遺忘指的是沒有被人訪問過。
總結一下,LRU就是當你記憶體中資料到達指定容量的時候,LRU選擇將最長時間沒有被使用過的那個鍵值對從記憶體中移除。

二、 明白什麼是LRU演算法之後,我們來看看該怎麼實現它?
1 我們需要儲存<K,V>鍵值對
2 可以指定可以儲存多少個鍵值對
3 當儲存容量滿了以後,移除最長時間沒有被訪問過的鍵值對
分析一下上面的需求

  • 儲存鍵值對:第一想法是使用HashMap儲存,畢竟HashMap是為“儲存鍵值對而生”。
  • 指定容量:這也容易,每次向map中put鍵值對後,就檢查map的容量是否超出了指定的容量,如果超出了,從map中移除一個元素即可。
  • 移除誰?難點在這裡,當map在put操作之後超出了指定容量,我們移除誰?怎麼找出那個“潛水”時間最長的鍵值對呢?第一個想法是我們需要維護一個有序的連結串列,這個連結串列從前到後“潛水”時間越來越短。這樣當map的容量滿了以後我們只需要獲取連結串列頭部元素,然後從map中移除該鍵值對即可,這也說明了連結串列節點中需要包含對應鍵值對的key,否則找到連結串列頭結點後如何移除呢?
  • 為什麼使用連結串列?容量指定的情況下,為什麼不使用陣列呢?原因如下,“潛水”時間最長的鍵值對經常傳送變化,也就是連結串列中元素順序經常發生改變,另外一方面,連結串列的插入和操作刪除效率要比陣列高。
  • 如何維護連結串列節點的順序?可以這樣,當一個“潛水”的鍵值對被訪問後,把該節點從連結串列中移除,然後把這個節點插入到連結串列末尾,這樣就可以保證連結串列第一個元素永遠是“潛水”時間最長的鍵值對;連結串列末尾的元素永遠是最近被訪問過的元素。這樣當map超出指定容量之後只需要刪除連結串列頭的鍵值對即可。
public class LRUCache {
	static class Node
{ //鍵值對 private int key; private int value; //維護“潛水”鍵值對,雙向連結串列 private Node pre; private Node next; //構造器 public Node(){} public Node(int key,int value){ this.key = key; this.value = value; } } private int cap;//指定的容量 //保留“潛水”雙向連結串列的頭尾指標 private Node head; private Node tail; private HashMap<Integer,Node> map;//儲存鍵值對的map public LRUCache(int capacity){ this.cap = capacity; head = new Node();//初始化頭尾節點,這裡的頭結點是輔助節點,head節點不儲存任何有效元素 tail = head; map = new HashMap<>((int)(cap/0.75)+1);//構造器初始容量這樣設定可以保證map不會發生擴容 } //將指定節點從連結串列中刪除 private void removeNode(Node cur){ if(cur == tail){ tail = tail.pre; tail.next = null; cur.pre = null; }else{ cur.pre.next = cur.next; cur.next.pre = cur.pre; cur.pre = null; cur.next = null; } } //將指定節點追加到連結串列末尾 private void add(Node cur){ tail.next = cur; cur.pre = tail; tail = cur; } //訪問一個鍵值對 public int get(int key){ Node cur = map.get(key); if(cur == null){//不存在這個key return -1; }else{ //存在,含義是當前潛水節點已經被訪問了,將這個節點新增到連結串列末尾 removeNode(cur); add(cur); return cur.value; } } //儲存一個鍵值對 public void put(int key,int value){ Node cur = map.get(key); if(cur == null){//put前不存在這個key cur = new Node(key,value); //將該鍵值對移動到連結串列末尾 map.put(key, cur); add(cur); //超出了容量,移除連結串列頭結點後面那個元素(頭結點是輔助節點) if(map.size()>cap && head!=tail){ Node outDate = head.next; removeNode(outDate); map.remove(outDate.key); } }else{ //put之前已經存在,將這個鍵值對移到連結串列末尾即可 removeNode(cur); add(cur); cur.value = value; //更新這個key的值 } } }

說明:

LinkedHashMap其實是可以直接支援LRU的,面試的時候可以提及這一點,但是面試官不會支援你使用LinkedHashMap實現LRU。實質LinkedHashMap底層實現也是類似原理,使用LinkedHashMap實現LRU程式碼如下:

//繼承一下LinkedHashMap這個類,
//使用LinkedHashMap實現LRU演算法
class LRULinkedHashMap<K,V> extends LinkedHashMap<K,V>{
      //定義快取的容量
      private int capacity;
      //帶引數的構造器   
      LRULinkedHashMap(int capacity){
          //呼叫LinkedHashMap的構造器
          super(capacity,0.75f,true);
          //傳入指定的快取最大容量
          this.capacity=capacity;
      }
     //返回true就會移除“潛水”時間最長的鍵值對
      @Override
      public boolean removeEldestEntry
         (Map.Entry<K, V> eldest){
         //引數eldest就是“潛水”時間最長的鍵值對,可以獲得對應的
         //key,value
          return size()>capacity;
      }  
  }

參考