今天为你们分享很出名的LRU算法,第一讲共包括4节。java
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
首先说一下LRUCache的示例解释一下。
经过上面的分析你们应该都能理解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); } } }
上文完成了我们本身的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是如何实现的呢?请关注下一期的内容~
文章来源:本文由小浩算法受权转载