乾貨|漫畫算法:LRU從實現到應用層層剖析(第一講)

今天爲你們分享很出名的LRU算法,第一講共包括4節。java

  • LRU概述
  • LRU使用
  • LRU實現
  • Redis近LRU概述

第一部分:LRU概述

LRU是Least Recently Used的縮寫,譯爲最近最少使用。它的理論基礎爲「最近使用的數據會在將來一段時期內仍然被使用,已經好久沒有使用的數據大機率在將來很長一段時間仍然不會被使用」因爲該思想很是契合業務場景 ,而且能夠解決不少實際開發中的問題,因此咱們常常經過LRU的思想來做緩存,通常也將其稱爲LRU緩存機制。由於剛好leetcode上有這道題,因此我乾脆把題目貼這裏。可是對於LRU而言,但願你們不要侷限於本題(你們不用擔憂學不會,我但願能作一個全網最簡單的版本,但願能夠堅持看下去!)下面,咱們一塊兒學習一下。node

題目:運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持如下操做:獲取數據 get 和 寫入數據 put 。算法

獲取數據 get(key) - 若是密鑰 (key) 存在於緩存中,則獲取密鑰的值(老是正數),不然返回 -1。

寫入數據 put(key, value) - 若是密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據以前刪除最近最少使用的數據值,從而爲新的數據值留出空間。緩存

進階:你是否能夠在 O(1) 時間複雜度內完成這兩種操做?數據結構

示例:app

LRUCache cache = new LRUCache( 2 / 緩存容量 / );

cache.put(1, 1);less

cache.put(2, 2);函數

cache.get(1); // 返回 1學習

cache.put(3, 3); // 該操做會使得密鑰 2 做廢優化

cache.get(2); // 返回 -1 (未找到)

cache.put(4, 4); // 該操做會使得密鑰 1 做廢

cache.get(1); // 返回 -1 (未找到)

cache.get(3); // 返回 3

cache.get(4); // 返回 4

第二部分:LRU使用

首先說一下LRUCache的示例解釋一下。

  • 第一步:咱們申明一個LRUCache,長度爲2

  • 第二步:咱們分別向cache裏邊put(1,1)和put(2,2),這裏由於最近使用的是2(put也算做使用)因此2在前,1在後。

  • 第三步:咱們get(1),也就是咱們使用了1,因此須要將1移到前面。

  • 第四步:此時咱們put(3,3),由於2是最近最少使用的,因此咱們須要將2進行做廢。此時咱們再get(2),就會返回-1。

  • 第五步:咱們繼續put(4,4),同理咱們將1做廢。此時若是get(1),也是返回-1。

  • 第六步:此時咱們get(3),實際爲調整3的位置。

  • 第七步:同理,get(4),繼續調整4的位置。

第三部分:LRU 實現(層層剖析)

經過上面的分析你們應該都能理解LRU的使用了。如今咱們聊一下實現。LRU通常來說,咱們是使用雙向鏈表實現。這裏我要強調的是,其實在項目中,並不絕對是這樣。好比Redis源碼裏,LRU的淘汰策略,就沒有使用雙向鏈表,而是使用一種模擬鏈表的方式。由於Redis大可能是當內存在用(我知道能夠持久化),若是再在內存中去維護一個鏈表,就平添了一些複雜性,同時也會多耗掉一些內存,後面我會單獨拉出來Redis的源碼給你們分析,這裏不細說。

回到題目,爲何咱們要選擇雙向鏈表來實現呢?看看上面的使用步驟圖,你們會發現,在整個LRUCache的使用中,咱們須要頻繁的去調整首尾元素的位置。而雙向鏈表的結構,恰好知足這一點(再囉嗦一下,前幾天我恰好看了groupcache的源碼,裏邊就是用雙向鏈表來作的LRU,固然它裏邊作了一些改進。groupcache是memcache做者實現的go版本,若是有go的讀者,能夠去看看源碼,仍是有一些收穫。)

下面,咱們採用hashmap+雙向鏈表的方式進行實現。

首先,咱們定義一個LinkNode,用以存儲元素。由於是雙向鏈表,天然咱們要定義pre和next。同時,咱們須要存儲下元素的key和value。val你們應該都能理解,關鍵是爲何須要存儲key?舉個例子,好比當整個cache的元素滿了,此時咱們須要刪除map中的數據,須要經過LinkNode中的key來進行查詢,不然沒法獲取到key。

type LRUCache struct {
    m          map[int]*LinkNode
    cap        int
    head, tail *LinkNode
}

如今有了LinkNode,天然須要一個Cache來存儲全部的Node。咱們定義cap爲cache的長度,m用來存儲元素。head和tail做爲Cache的首尾。

type LRUCache struct {
    m          map[int]*LinkNode
    cap        int
    head, tail *LinkNode
}

接下來咱們對整個Cache進行初始化。在初始化head和tail的時候將它們鏈接在一塊兒。

func Constructor(capacity int) LRUCache {
    head := &LinkNode{0, 0, nil, nil}
    tail := &LinkNode{0, 0, nil, nil}
    head.next = tail
    tail.pre = head
    return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}

大概是這樣:

如今咱們已經完成了Cache的構造,剩下的就是添加它的API了。由於Get比較簡單,咱們先完成Get方法。這裏分兩種狀況考慮,若是沒有找到元素,咱們返回-1。若是元素存在,咱們須要把這個元素移動到首位置上去。

func (this *LRUCache) Get(key int) int {
     head := this.head
     cache := this.m
     if v, exist := cache[key]; exist {
         v.pre.next = v.next
         v.next.pre = v.pre
         v.next = head.next
         head.next.pre = v
         v.pre = head
        head.next = v
        return v.val
    } else {
        return -1
    }
}

大概就是下面這個樣子(倘若2是咱們get的元素)

咱們很容易想到這個方法後面還會用到,因此將其抽出。

func (this *LRUCache) moveToHead(node *LinkNode){
        head := this.head
        //從當前位置刪除
        node.pre.next = node.next
        node.next.pre = node.pre
        //移動到首位置
        node.next = head.next
        head.next.pre = node
        node.pre = head
        head.next = node
}

func (this *LRUCache) Get(key int) int {
    cache := this.m
    if v, exist := cache[key]; exist {
        this.moveToHead(v)
        return v.val
    } else {
        return -1
    }
}

如今咱們開始完成Put。實現Put時,有兩種狀況須要考慮。倘若元素存在,其實至關於作一個Get操做,也是移動到最前面(可是須要注意的是,這裏多了一個更新值的步驟)。

func (this *LRUCache) Put(key int, value int) {
    head := this.head
    tail := this.tail
    cache := this.m
    //倘若元素存在
    if v, exist := cache[key]; exist {
        //1.更新值
        v.val = value
        //2.移動到最前
        this.moveToHead(v)
    } else {
        //TODO
    }
}

倘若元素不存在,咱們將其插入到元素首,並把該元素值放入到map中。

func (this *LRUCache) Put(key int, value int) {
    head := this.head
    tail := this.tail
    cache := this.m
    //存在
    if v, exist := cache[key]; exist {
        //1.更新值
        v.val = value
        //2.移動到最前
        this.moveToHead(v)
    } else {
        v := &LinkNode{key, value, nil, nil}
        v.next = head.next
        v.pre = head
        head.next.pre = v
        head.next = v
        cache[key] = v
    }
}

可是咱們漏掉了一種狀況,若是剛好此時Cache中元素滿了,須要刪掉最後的元素。處理完畢,附上Put函數完整代碼。

func (this *LRUCache) Put(key int, value int) {
    head := this.head
    tail := this.tail
    cache := this.m
    //存在
    if v, exist := cache[key]; exist {
        //1.更新值
        v.val = value
        //2.移動到最前
        this.moveToHead(v)
    } else {
        v := &LinkNode{key, value, nil, nil}
        if len(cache) == this.cap {
            //刪除最後元素
            delete(cache, tail.pre.key)
            tail.pre.pre.next = tail
            tail.pre = tail.pre.pre
        }
        v.next = head.next
        v.pre = head
        head.next.pre = v
        head.next = v
        cache[key] = v
    }
}

最後,咱們完成全部代碼:

type LinkNode struct {
    key, val  int
    pre, next *LinkNode
}

type LRUCache struct {
    m          map[int]*LinkNode
    cap        int
    head, tail *LinkNode
}

func Constructor(capacity int) LRUCache {
    head := &LinkNode{0, 0, nil, nil}
    tail := &LinkNode{0, 0, nil, nil}
    head.next = tail
    tail.pre = head
    return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}

func (this *LRUCache) Get(key int) int {
    cache := this.m
    if v, exist := cache[key]; exist {
        this.moveToHead(v)
        return v.val
    } else {
        return -1
    }
}

func (this *LRUCache) moveToHead(node *LinkNode) {
    head := this.head
    //從當前位置刪除
    node.pre.next = node.next
    node.next.pre = node.pre
    //移動到首位置
    node.next = head.next
    head.next.pre = node
    node.pre = head
    head.next = node
}

func (this *LRUCache) Put(key int, value int) {
    head := this.head
    tail := this.tail
    cache := this.m
    //存在
    if v, exist := cache[key]; exist {
        //1.更新值
        v.val = value
        //2.移動到最前
        this.moveToHead(v)
    } else {
        v := &LinkNode{key, value, nil, nil}
        if len(cache) == this.cap {
            //刪除末尾元素
            delete(cache, tail.pre.key)
            tail.pre.pre.next = tail
            tail.pre = tail.pre.pre
        }
        v.next = head.next
        v.pre = head
        head.next.pre = v
        head.next = v
        cache[key] = v
    }
}

優化後:

type LinkNode struct {
    key, val  int
    pre, next *LinkNode
}

type LRUCache struct {
    m          map[int]*LinkNode
    cap        int
    head, tail *LinkNode
}

func Constructor(capacity int) LRUCache {
    head := &LinkNode{0, 0, nil, nil}
    tail := &LinkNode{0, 0, nil, nil}
    head.next = tail
    tail.pre = head
    return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}

func (this *LRUCache) Get(key int) int {
    cache := this.m
    if v, exist := cache[key]; exist {
        this.MoveToHead(v)
        return v.val
    } else {
        return -1
    }
}

func (this *LRUCache) RemoveNode(node *LinkNode) {
    node.pre.next = node.next
    node.next.pre = node.pre
}

func (this *LRUCache) AddNode(node *LinkNode) {
    head := this.head
    node.next = head.next
    head.next.pre = node
    node.pre = head
    head.next = node
}

func (this *LRUCache) MoveToHead(node *LinkNode) {
    this.RemoveNode(node)
    this.AddNode(node)
}

func (this *LRUCache) Put(key int, value int) {
    tail := this.tail
    cache := this.m
    if v, exist := cache[key]; exist {
        v.val = value
        this.MoveToHead(v)
    } else {
        v := &LinkNode{key, value, nil, nil}
        if len(cache) == this.cap {
            delete(cache, tail.pre.key)
            this.RemoveNode(tail.pre)
        }
        this.AddNode(v)
        cache[key] = v
    }
}

由於該算法過於重要,給一個Java版本的:

//java版本
public class LRUCache {
  class LinkedNode {
    int key;
    int value;
    LinkedNode prev;
    LinkedNode next;
  }

  private void addNode(LinkedNode node) {
    node.prev = head;
    node.next = head.next;
    head.next.prev = node;
    head.next = node;
  }

  private void removeNode(LinkedNode node){
    LinkedNode prev = node.prev;
    LinkedNode next = node.next;
    prev.next = next;
    next.prev = prev;
  }

  private void moveToHead(LinkedNode node){
    removeNode(node);
    addNode(node);
  }

  private LinkedNode popTail() {
    LinkedNode res = tail.prev;
    removeNode(res);
    return res;
  }

  private Hashtable<Integer, LinkedNode> cache = new Hashtable<Integer, LinkedNode>();
  private int size;
  private int capacity;
  private LinkedNode head, tail;

  public LRUCache(int capacity) {
    this.size = 0;
    this.capacity = capacity;
    head = new LinkedNode();
    tail = new LinkedNode();
    head.next = tail;
    tail.prev = head;
  }

  public int get(int key) {
    LinkedNode node = cache.get(key);
    if (node == null) return -1;
    moveToHead(node);
    return node.value;
  }

  public void put(int key, int value) {
    LinkedNode node = cache.get(key);

    if(node == null) {
      LinkedNode newNode = new LinkedNode();
      newNode.key = key;
      newNode.value = value;
      cache.put(key, newNode);
      addNode(newNode);
      ++size;
      if(size > capacity) {
        LinkedNode tail = popTail();
        cache.remove(tail.key);
        --size;
      }
    } else {
      node.value = value;
      moveToHead(node);
    }
  }
}

第四部分:Redis 近LRU 介紹

上文完成了我們本身的LRU實現,如今如今聊一聊Redis中的近似LRU。因爲真實LRU須要過多的內存(在數據量比較大時),因此Redis是使用一種隨機抽樣的方式,來實現一個近似LRU的效果。說白了,LRU根本只是一個預測鍵訪問順序的模型。

在Redis中有一個參數,叫作 「maxmemory-samples」,是幹嗎用的呢?

# LRU and minimal TTL algorithms are not precise algorithms but approximated 
# algorithms (in order to save memory), so you can tune it for speed or 
# accuracy. For default Redis will check five keys and pick the one that was 
# used less recently, you can change the sample size using the following 
# configuration directive. 
# 
# The default of 5 produces good enough results. 10 Approximates very closely 
# true LRU but costs a bit more CPU. 3 is very fast but not very accurate. 
# 
maxmemory-samples 5

上面咱們說過了,近似LRU是用隨機抽樣的方式來實現一個近似的LRU效果。這個參數其實就是做者提供了一種方式,可讓咱們人爲干預樣本數大小,將其設的越大,就越接近真實LRU的效果,固然也就意味着越耗內存。(初始值爲5是做者默認的最佳)

這個圖解釋一下,綠色的點是新增長的元素,深灰色的點是沒有被刪除的元素,淺灰色的是被刪除的元素。最下面的這張圖,是真實LRU的效果,第二張圖是默認該參數爲5的效果,能夠看到淺灰色部分和真實的契合仍是不錯的。第一張圖是將該參數設置爲10的效果,已經基本接近真實LRU的效果了。

因爲時間關係本文基本就說到這裏。那Redis中的近似LRU是如何實現的呢?請關注下一期的內容~

文章來源:本文由小浩算法受權轉載
相關文章
相關標籤/搜索