今天为大家分享很出名的LRU算法,第一讲共包括4节。

  • LRU概述
  • LRU使用
  • LRU实现
  • Redis近LRU概述

第一部分:LRU概述

LRU是Least Recently Used的缩写,译为最近最少使用。它的理论基础为“最近使用的数据会在未来一段时期内仍然被使用,已经很久没有使用的数据大概率在未来很长一段时间仍然不会被使用”由于该思想非常契合业务场景 ,并且可以解决很多实际开发中的问题,所以我们经常通过LRU的思想来作缓存,一般也将其称为LRU缓存机制。因为恰好leetcode上有这道题,所以我干脆把题目贴这里。但是对于LRU而言,希望大家不要局限于本题(大家不用担心学不会,我希望能做一个全网最简单的版本,希望可以坚持看下去!)下面,我们一起学习一下。

题目:运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。

写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

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

cache.put(1, 1);

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{, , nil, nil}
tail := &LinkNode{, , 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 -
}
}

大概就是下面这个样子(假若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 -
}
}

现在我们开始完成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{, , nil, nil}
tail := &LinkNode{, , 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 -
}
} 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{, , nil, nil}
tail := &LinkNode{, , 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 -
}
} 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 = ;
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 -;
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 produces good enough results. Approximates very closely
# true LRU but costs a bit more CPU. is very fast but not very accurate.
#
maxmemory-samples

上面我们说过了,近似LRU是用随机抽样的方式来实现一个近似的LRU效果。这个参数其实就是作者提供了一种方式,可以让我们人为干预样本数大小,将其设的越大,就越接近真实LRU的效果,当然也就意味着越耗内存。(初始值为5是作者默认的最佳)

这个图解释一下,绿色的点是新增加的元素,深灰色的点是没有被删除的元素,浅灰色的是被删除的元素。最下面的这张图,是真实LRU的效果,第二张图是默认该参数为5的效果,可以看到浅灰色部分和真实的契合还是不错的。第一张图是将该参数设置为10的效果,已经基本接近真实LRU的效果了。

由于时间关系本文基本就说到这里。那Redis中的近似LRU是如何实现的呢?请关注下一期的内容~

文章来源:本文由小浩算法授权转载

干货|漫画算法:LRU从实现到应用层层剖析(第一讲)的更多相关文章

  1. 0基础算法基础学算法 第八弹 递归进阶,dfs第一讲

    最近很有一段时间没有更新了,主要是因为我要去参加一个重要的考试----小升初!作为一个武汉的兢兢业业的小学生当然要去试一试我们那里最好的几个学校的考试了,总之因为很多的原因放了好久的鸽子,不过从今天开 ...

  2. 【C#实现漫画算法系列】-判断 2 的乘方

    微信上关注了算法爱好者这个公众号,有一个漫画算法系列的文章生动形象,感觉特别好,给大家推荐一下(没收过广告费哦),原文链接:漫画算法系列.也看到了许多同学用不同的语言来实现算法,作为一枚C#资深爱好的 ...

  3. LRU算法 - LRU Cache

    这个是比较经典的LRU(Least recently used,最近最少使用)算法,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”. 一般应 ...

  4. 干货 | NLP算法岗大厂面试经验与路线图分享

    最近有好多小伙伴要面经(还有个要买简历的是什么鬼),然鹅真的没有整理面经呀,真的木有时间(。 ́︿ ̀。).不过话说回来,面经有多大用呢?最起码对于NLP岗位的面试来说,作者发现根本不是面经中说的样子 ...

  5. 逆向实用干货分享,Hook技术第一讲,之Hook Windows API

    逆向实用干货分享,Hook技术第一讲,之Hook Windows API 作者:IBinary出处:http://www.cnblogs.com/iBinary/版权所有,欢迎保留原文链接进行转载:) ...

  6. 聊聊缓存淘汰算法-LRU 实现原理

    前言 我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来.缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据.常用淘 ...

  7. 缓存淘汰算法--LRU算法

    1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也 ...

  8. 操作系统 页面置换算法LRU和FIFO

    LRU(Least Recently Used)最少使用页面置换算法,顾名思义,就是替换掉最少使用的页面. FIFO(first in first out,先进先出)页面置换算法,这是的最早出现的置换 ...

  9. 近期最久未使用页面淘汰算法———LRU算法(java实现)

    请珍惜小编劳动成果,该文章为小编原创,转载请注明出处. LRU算法,即Last Recently Used ---选择最后一次訪问时间距离当前时间最长的一页并淘汰之--即淘汰最长时间没有使用的页 依照 ...

随机推荐

  1. Hello World!(这不是第一篇)

    如题,这不是第一篇blog,但是为了表示这个闲置了1年多的blog现在被我正式启用了,我还是走个过场吧. #include <iostream> using namespace std; ...

  2. hexo及next主题修改

    通过npm uninstall <package>命令,你可以将node_modules目录下的某个依赖包移除: 1 npm uninstall 包名 要从package.json文件的依 ...

  3. 1,Hadoop知识储备

    Hadoop初学思维导图 1,Hadoop ··· Hadoop:     Hadoop的核心由HDFS和MapReduce组成.HDFS是分布式文件系统,是Hadoop生态圈的分布式数据存储基石:M ...

  4. TCP传输连接管理

    TCP传输连接管理 一.传输连接的三个阶段 1.1.概述 传输连接就有三个阶段,即:连接建立.数据传送和连接释放. 连接建立过程中要解决以下三个问题: 要使每一方能够确知对方的存在. 要允许双方协商一 ...

  5. python列表解析补充:

    python列表解析补充: # 补充: f = [x + y for x in 'ABCDE' for y in '1234567'] print(f) test = [] for x in 'ABC ...

  6. 【读书笔记】https://source.android.google.cn/compatibility/tests?hl=en

    AuthorBlog:秋城https://www.cnblogs.com/houser0323/ Android Platform Testing This content is geared tow ...

  7. Sentinel Slot扩展实践-流控熔断预警实现

    前言 前几天公司生产环境一个服务由于流量上升触发了 Sentinel 的流控机制,然后用户反馈访问慢,定位发现是 task 定时任务导致,后面 task 优化之后发布,流量恢复正常. 这是一个再正常不 ...

  8. 每个 JavaScript 工程师都应当知道的 10 个面试题

    1. 能说出来两种对于 JavaScript 工程师很重要的编程范式么? JavaScript 是一门多范式(multi-paradigm)的编程语言,它既支持命令式(imperative)/面向过程 ...

  9. 使用R进行空间自相关检验

    「全局溢出」当一个区域的特征变化影响到所有区域的结果时,就会产生全局溢出效应.这甚至适用于区域本身,因为影响可以传递到邻居并返回到自己的区域(反馈).具体来说,全球溢出效应影响到邻居.邻居到邻居.邻居 ...

  10. Block as a Value for SQL over NoSQL

    作者 Yang Cao,Wenfei Fan,Tengfei Yuan University of Edinburgh,Beihang University SICS, Shenzhen Univer ...