手撕LRU缓存了解一下
面试官:来了,老弟,LRU缓存实现一下?
我:直接LinkedHashMap就好了。
面试官:不要用现有的实现,自己实现一个。
我:.....
面试官:回去等消息吧....
大家好,我是程序员学长,今天我们来聊一聊LRU缓存问题。
Tips: LRU在计算机软件中无处不在,希望大家一定要了解透彻。
问题描述
设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
1. set(key, value):将记录(key, value)插入该结构
2. get(key):返回key对应的value值
分析问题
根据问题描述,我们可以知道LRU包含两种操作,即Set和Get操作。
对于Set操作来说,分为两种情况。
1. 缓存中已经存在。把缓存中的该元素移动到缓存头部。
2. 如果缓存中不存在。把该元素添加到缓存头部。如果此时缓存的大小超过限制的大小,需要删除缓存中末尾的元素。
对于Get操作来着,也分为两种情况。
- 缓存中存在。把缓存中的该元素移动到缓存头部。并返回对应的value值。
- 缓存中不存在。直接返回-1。
综上所述:对于一个LRU缓存结构来说,主要需要支持以下三种操作。
- 查找一个元素。
- 在缓存末尾删除一个元素。
- 在缓存头部添加一个元素。
所以,我们最容易想到的就是使用一个链表来实现LRU缓存。
我们可以维护一个有序的单链表,越靠近链表尾部的结点是越早访问的。
当我们进行Set操作时,我们从链表头开始顺序遍历。遍历的结果有两种情况。
- 如果此数据之前就已经被缓存在链表中,我们遍历得到这个数据对应的结点,然后将其从这个位置移动到链表的头部。
- 如果此数据不在链表中,又会分为两种情况。如果此时缓存链表没有满,我们直接将该结点插入链表头部。如果此时缓存链表已经满了,我们从链表尾部删除一个结点,然后将新的数据结点插入到链表头部。
当我们进行Get操作时,我们从链表头开始顺序遍历。遍历的结果有两种情况。
- 如果此数据之前就已经被缓存在链表中,我们遍历得到这个数据对应的结点,然后将其从这个位置移动到链表的头部。
- 如果此数据之前不在缓存中,我们直接返回-1。
下面我们来看一下代码如何实现。
class LinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.next = None
class LRUCache():
def __init__(self, capacity: int):
# 使用伪头部节点
self.capacity=capacity
self.head = LinkedNode()
self.head.next=None
self.size = 0
def get(self, key: int) -> int:
cur=self.head.next
pre=self.head
while cur!=None:
if cur.key==key:
pre.next = cur.next
cur.next = self.head.next
self.head.next = cur
break
pre=pre.next
cur=cur.next
if cur!=None:
return cur.value
else:
return -1
def put(self, key: int, value: int) -> None:
cur = self.head.next
pre = self.head
#缓存没有元素,直接添加
if cur==None:
node = LinkedNode()
node.key = key
node.value = value
self.head.next = node
self.size = self.size + 1
return
#缓存有元素,判断是否存在于缓存中
while cur!=None:
#表示已经存在
if cur.key == key:
#把该元素反正链表头部
cur.value=value
pre.next = cur.next
cur.next = self.head.next
self.head.next = cur
break
#代表当前元素时最后一个元素
if cur.next==None:
#如果此时缓存已经满了,淘汰最后一个元素
if self.size==self.capacity:
pre.next=None
self.size=self.size-1
node=LinkedNode()
node.key=key
node.value=value
node.next=self.head.next
self.head.next=node
self.size=self.size+1
break
pre = pre.next
cur=cur.next
这样我们就用链表实现了一个LRU缓存,我们接下来分析一下缓存访问的时间复杂度。对于Set来说,不管缓存有没有满,我们都需要遍历一遍链表,所以时间复杂度是O(n)。对于Get操作来说,也是需要遍历一遍链表,所以时间复杂度也是O(n)。
优化
从上面的分析,我们可以看到。如果用单链表来实现LRU,不论是Set还是Get操作,都需要遍历一遍链表,来查找当前元素是否在缓存中,时间复杂度为O(n),那我们可以优化吗?我们知道,使用hash表,我们查找元素的时间复杂度可以减低到O(1),如果我们可以用hash表,来替代上述的查找操作,那不就可以减低时间复杂度吗?根据这个逻辑,所以我们采用hash表和链表的组合方式来实现一个高效的LRU缓存。
class LinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = dict()
self.head = LinkedNode()
self.tail = LinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
self.capacity = capacity
self.size = 0
def get(self, key: int):
#如果key不存在,直接返回-1
if key not in self.cache:
return -1
#通过hash表定位位置,然后删除,省去遍历查找过程
node = self.cache[key]
self.moveHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果key不存在,创建一个新的节点
node = LinkedNode(key, value)
# 添加进哈希表
self.cache[key] = node
self.addHead(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除双向链表的尾部节点
removed = self.removeTail()
# 删除哈希表中对应的项
self.cache.pop(removed.key)
self.size -= 1
else:
node = self.cache[key]
node.value = value
self.moveHead(node)
def addHead(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def removeNode(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def moveHead(self, node):
self.removeNode(node)
self.addHead(node)
def removeTail(self):
node = self.tail.prev
self.removeNode(node)
return node
总结
LRU缓存不论在工作中还是面试中,我们都会经常碰到。希望这篇文章能对你有所帮助。
今天,我们就聊到这里。更多有趣知识,请关注公众号【程序员学长】。我给你准备了上百本学习资料,包括python、java、数据结构和算法等。如果需要,请关注公众号【程序员学长】,回复【资料】,即可得。
你知道的越多,你的思维也就越开阔,我们下期再见。
手撕LRU缓存了解一下的更多相关文章
- 手撕LRU缓存
面试官:来了,老弟,LRU缓存实现一下? 我:直接LinkedHashMap就好了. 面试官:不要用现有的实现,自己实现一个. 我:..... 面试官:回去等消息吧.... 大家好,我是程序员学长,今 ...
- 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...
- HashMap+双向链表手写LRU缓存算法/页面置换算法
import java.util.Hashtable; class DLinkedList { String key; //键 int value; //值 DLinkedList pre; //双向 ...
- Java:手写幼儿园级线程安全LRU缓存X探究影响命中率的因素
最近遇到一个需求,需要频繁访问数据库,但是访问的内容只是 id + 名称 这样的简单键值对. 频繁的访问数据库,网络上和内存上都会给数据库服务器带来不小负担. 于是打算写一个简单的LRU缓存来缓存这样 ...
- Netty实现高性能IOT服务器(Groza)之手撕MQTT协议篇上
前言 诞生及优势 MQTT由Andy Stanford-Clark(IBM)和Arlen Nipper(Eurotech,现为Cirrus Link)于1999年开发,用于监测穿越沙漠的石油管道.目标 ...
- 面试挂在了 LRU 缓存算法设计上
好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存 ...
- 阿里面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存,我蒙了!
目录 1. LRU 缓存介绍 2. ConcurrentLinkedQueue简单介绍 3. ReadWriteLock简单介绍 4.ScheduledExecutorService 简单介绍 5. ...
- LRU缓存实现(Java)
LRU Cache的LinkedHashMap实现 LRU Cache的链表+HashMap实现 LinkedHashMap的FIFO实现 调用示例 LRU是Least Recently Used 的 ...
- 转: LRU缓存介绍与实现 (Java)
引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本, ...
随机推荐
- vue源码解析之observe
一. vue文档中有"由于 JavaScript 的限制,Vue 不能检测以下数组的变动",是否真是由于JavaScript的限制,还是出于其他原因考虑 当你利用索引直接设置一个数 ...
- js之检测浏览器
getBrowser () { let ua = navigator.userAgent.toLocaleLowerCase() let browserType = null if (ua.match ...
- Jenkins(8080)未授权访问
下载地址http://mirrors.jenkins.io/debian/ 测试 浏览器访问http://localhost:8080/manage可以直接访问 点击脚本命令行 println &qu ...
- 关于Asp.Net MVC html.renderaction传递参数
View视图 @{html.renderaction("控制器方法名","控制器名称",new{params1="参数值"})} Contr ...
- Redis缓存哪些事儿
一提到Redis缓存,我们不得不了解的三个问题就是:缓存雪崩.缓存击穿和缓存穿透.这三个问题一旦发生,会导致大量的请求直接请求到数据库层.如果并发压力大,就会导致数据库崩溃.那p0级的故障是没跑了. ...
- 关于shell脚本——条件测试、if语句、case语句
目录 一.条件测试 1.1.表达说明 1.2.test命令 文件测试 1.3.整数值比较 1.4.字符串比较 1.5.逻辑测试 二.if语句 2.1.单分支结构 2.2.双分支结构 2.3.多分支结构 ...
- 【XSS-labs】level 6-10
Level 6 查看源码:对URL中的传参进行了HTML实体化转义,搜索框中的值对 src\onxxxxx\data\href进行了限制. 采用大小写绕过,在搜索框输入payload,注意闭合inpu ...
- mybaits进阶01
在以上mybait入门的改进(增加了接口让增删改查 后期跟容易) 注意:主配置文件和映射配置文件内容不变,但是映射文件要和对应接口放于同目录下并且名称必须相同 一.接口创建 public interf ...
- Java小题,通过JNI调用本地C++共享库中的对应方法实现杨辉三角的绘制
1.在Eclipse中配置Javah,配置如下 位置是你javah.exe在你电脑磁盘上的路径 位置:C:\Program Files\Java\jdk1.8.0_112\bin\javah.exe ...
- ReentrantLock中的Condition(等待和唤醒)
Condition 类的 awiat 方法和 Object 类的 wait 方法等效 Condition 类的 signal 方法和 Object 类的 notify 方法等效 Condition 类 ...