干货|漫画算法: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是如何实现的呢?请关注下一期的内容~

文章来源:本文由小浩算法受权转载
相关文章
相关标签/搜索