干货|漫画算法: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 ---选择最后一次訪问时间距离当前时间最长的一页并淘汰之--即淘汰最长时间没有使用的页 依照 ...
随机推荐
- 为何滴滴会走Uber之路,研发无人驾驶?
近日,滴滴出行宣布完成新一轮超过55亿美元融资,以支持其全球化战略的推进和前沿技术领域的投资.其中,无人驾驶汽车将是这笔资金重要的投资方向.此前,滴滴在全球范围内的追赶对象Uber不断在无人汽车领域发 ...
- TDA2050功率放大器研究
音频功率放大模块(以下简称功放)用于处理模拟信号,将功率较低的输入信号进行线性放大,输出大功率的信号以驱动换能器.通常,电子发烧友自己设计功放,与各类音源和喇叭匹配,以得到满意的音响效果.在测试中,实 ...
- 关于.net MVC中主视图和分部视图的数据共享遇到的问题
今天在开发web时因为调用到的分部视图需要有个隐藏域.然后因为当我们第一次调用分部视图时,是用 @Html.Partial("DetailDataPart")在主视图里把它嵌进去主 ...
- MVC01
1.Controller 1) 添加: 在Controller目录右键进行添加,出现很多模式供选择,选择空的Controller,命名后新建.新建后Views 目录将同步生成相应名称的视图文件目录 均 ...
- jdbc对 数据库的数据进行增删改(两个类)
1.方法类 package com.com; import java.sql.Connection;import java.sql.DriverManager;import java.sql.Resu ...
- python之路-基本数据类型之int整数和bool值
1.int整数 #整数:主要用来进行数学运算,在python3中所有的整数都是int类型, #整数可以进行的操作有:bit_length(),计算整数在内存中占用的二进制码的长度 #例子:查看整数在内 ...
- Vue+axios(interceptors) 实现http拦截 + router路由拦截 (双拦截)+ 请求自带loading效果
axios interceptors 拦截器 //interceptors.js // vue axios配置 发起请求加载loading请求结束关闭loading // http request 请 ...
- vue项目npm run dev 报错Uncaught SyntaxError: Unexpected token <
目前代码所处位置是micro分支,该分支是从dev分支直接拉下来进行npm run dev的,而dev分支是可以正常运行的,网上的诸多解释是babel转义时候报错,其实对比可见,两个分支不同的地方应该 ...
- golang 学习之路 string转换为其他类型 其他类型转换为string
将其他值转换为string 一般常用fmt.Sprintf(格式,转换的值) // 使用fmt.Sprintf 转换所有的类型为string 使用 这是第一种 // 注意在sprintf使用中需要注意 ...
- javaee作业
一.单选题(共5题,50.0分) 1 在SqlSession对象的openSession()方法中,不能作为参数executorType的可选值 的是( ). A. ExecutorTyp ...