干货|漫画算法:LRU从实现到应用层层剖析(第一讲)
今天为大家分享很出名的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从实现到应用层层剖析(第一讲)的更多相关文章
- 0基础算法基础学算法 第八弹 递归进阶,dfs第一讲
最近很有一段时间没有更新了,主要是因为我要去参加一个重要的考试----小升初!作为一个武汉的兢兢业业的小学生当然要去试一试我们那里最好的几个学校的考试了,总之因为很多的原因放了好久的鸽子,不过从今天开 ...
- 【C#实现漫画算法系列】-判断 2 的乘方
微信上关注了算法爱好者这个公众号,有一个漫画算法系列的文章生动形象,感觉特别好,给大家推荐一下(没收过广告费哦),原文链接:漫画算法系列.也看到了许多同学用不同的语言来实现算法,作为一枚C#资深爱好的 ...
- LRU算法 - LRU Cache
这个是比较经典的LRU(Least recently used,最近最少使用)算法,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”. 一般应 ...
- 干货 | NLP算法岗大厂面试经验与路线图分享
最近有好多小伙伴要面经(还有个要买简历的是什么鬼),然鹅真的没有整理面经呀,真的木有时间(。 ́︿ ̀。).不过话说回来,面经有多大用呢?最起码对于NLP岗位的面试来说,作者发现根本不是面经中说的样子 ...
- 逆向实用干货分享,Hook技术第一讲,之Hook Windows API
逆向实用干货分享,Hook技术第一讲,之Hook Windows API 作者:IBinary出处:http://www.cnblogs.com/iBinary/版权所有,欢迎保留原文链接进行转载:) ...
- 聊聊缓存淘汰算法-LRU 实现原理
前言 我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来.缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据.常用淘 ...
- 缓存淘汰算法--LRU算法
1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也 ...
- 操作系统 页面置换算法LRU和FIFO
LRU(Least Recently Used)最少使用页面置换算法,顾名思义,就是替换掉最少使用的页面. FIFO(first in first out,先进先出)页面置换算法,这是的最早出现的置换 ...
- 近期最久未使用页面淘汰算法———LRU算法(java实现)
请珍惜小编劳动成果,该文章为小编原创,转载请注明出处. LRU算法,即Last Recently Used ---选择最后一次訪问时间距离当前时间最长的一页并淘汰之--即淘汰最长时间没有使用的页 依照 ...
随机推荐
- JavaScript是如何工作的(一)
简评:JavaScript 是越来越受欢迎了,很多团队都在采用这些语言工作.前端.后端.嵌入式设备等等,都可以看见它的身影.虽然我们知其然,但又知其所以然吗? 大家应该都知道 JavaScript 是 ...
- 时尚起义开源话题微博系统 v.0.4.5 上传漏洞
漏洞出现在/action/upload.php文件中 <?php /** ** **By QINIAO **/ !defined('QINIAO_ROOT') && exit(' ...
- Js对于数组去重提高效率一些心得
最近在找工作,好几次面试都问过数组去重的问题.虽然问的都不一样,但是核心思想是没有变的. 第一种是比较常规的方法 思路: 构建一个新的数组存放结果 for循环中每次从原数组中取出一个元素,用这个元素循 ...
- Redis(2)——跳跃表
一.跳跃表简介 跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文<Skip lists: a probabilistic alternative to ba ...
- 前阿里数据库专家总结的MySQL里的各种锁(下篇)
在上篇中,我们介绍了MySQL中的全局锁和表锁. 今天,我们专注于介绍一下行锁,这个在日常开发和面试中常常困扰我们的问题. 1.行锁基础 由于全局锁和表锁对增删改查的性能都会有较大影响,所以,我们自然 ...
- BUI Webapp用于项目中的一点小心得
接触BUI也有一段时间,也用在了移动端的项目开发中,总的来说,该框架用起来也挺灵活的,控件可以自由定制,前提是自己能认真地学习该框架的api,因为api里面说的东西比较详细,如果没有仔细看的,可能有些 ...
- scroll-view组件bindscroll实例应用:自定义滚动条
我们知道scroll-view组件作为滑动控件非常好用,而有时候我们想放置一个跟随滚动位置来跟进的滚动条,但又不想用滚动条api该怎么办呢?(当然是自己写一个呗还能怎么办[自黑冷漠脸])嗯,没错.自己 ...
- Java多态实现的机制
Java提供了编译时多态和运行时多态两种多态机制.前者是通过方法重载实现的,后者是通过方法的覆盖实现的. 在方法覆盖中,子类可以覆盖父类的方法,因此同类的方法会在父类与子类中有着不同的表现形式. 在J ...
- 分享一次C#调用Delphi编写Dll程序
1.前言: 最近接手了一个项目需要和Delphi语言编写的一个系统进行一些接口的对接,数据在传输过程中采用Des加密方式,因为Delphi 平台的加密方式和C#平台的加密方式不互通,所以采用的方式是C ...
- Go组件学习:如何读取ini配置文件
代码示例全部保存在,欢迎star:https://github.com/EnochZg/golang-examples 安装组件 go get gopkg.in/ini.v1 使用 先创建ini后缀的 ...