面试官:如何实现LRU?你学会了吗?
面试官:来了,老弟,LRU缓存实现一下?
我:直接LinkedHashMap就好了。
面试官:不要用现有的实现,自己实现一个。
我:.....
面试官:回去等消息吧....
大家好,我是程序员学长,今天我们来聊一聊LRU缓存问题。
Tips: LRU在计算机软件中无处不在,希望大家一定要了解透彻。
问题描述
设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
1. set(key, value):将记录(key, value)插入该结构
2. get(key):返回key对应的value值
分析问题
根据问题描述,我们可以知道LRU包含两种操作,即Set和Get操作。
对于Set操作来说,分为两种情况。
- 缓存中已经存在。把缓存中的该元素移动到缓存头部。
- 如果缓存中不存在。把该元素添加到缓存头部。如果此时缓存的大小超过限制的大小,需要删除缓存中末尾的元素。
对于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缓存不论在工作中还是面试中,我们都会经常碰到。希望这篇文章能对你有所帮助。
今天,我们就聊到这里。更多有趣知识,请关注公众号【程序员学长】。
你知道的越多,你的思维也就越开阔,我们下期再见。
面试官:如何实现LRU?你学会了吗?的更多相关文章
- 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...
- 阿里面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存,我蒙了!
目录 1. LRU 缓存介绍 2. ConcurrentLinkedQueue简单介绍 3. ReadWriteLock简单介绍 4.ScheduledExecutorService 简单介绍 5. ...
- Spring第三天,详解Bean的生命周期,学会后让面试官无话可说!
点击下方链接回顾往期 不要再说不会Spring了!Spring第一天,学会进大厂! Spring第二天,你必须知道容器注册组件的几种方式!学废它吊打面试官! 今天讲解Spring中Bean的生命周期. ...
- 面试官的七种武器:Java篇
起源 自己经历过的面试也不少了,互联网的.外企的,都有.总结一下这些面试的经验,发现面试官问的问题其实不外乎几个大类,玩不出太多新鲜玩意的.细细想来,面试官拥有以下七种武器.恰似古龙先生笔下的武侠世界 ...
- 走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串
原文:走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串 对大量数据进行查询时,可以应用到索引技术.索引是一种特殊类型的数据库对象,它保存着数据表中一列或者多列的排序结果,有 ...
- 金三银四,如何征服面试官,拿到Offer
又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往年「冷」一些,形式更加严峻一些,大家多多少少可能都听到或看到一些信息,就是好多公司在优化裁 ...
- 如何征服面试官,拿到Offer [转]
转自 https://my.oschina.net/cccyb/blog/3012768 又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往 ...
- 面试官:你了解过Redis对象底层实现吗
上一章我们讲了Redis的底层数据结构,不了解的人可能会有疑问:这个和平时用的五大对象有啥关系呢?这一章我们就主要解释他们所建立的联系. 看这个文件之前,如果对ziplist.skiplist.int ...
- 《吊打面试官》系列-Redis常见面试题(带答案)
你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源,有面试点思维导图,欢迎[Star]和[完善] 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在 ...
随机推荐
- python---replace函数
描述 Python replace() 方法把字符串中的 old(旧字符串) 替换成 new(新字符串),如果指定第三个参数max,则替换不超过 max 次. 语法 replace()方法语法: st ...
- 流畅的python-2
一.python数据结构 ----------------> () [] {} List 列表[] 可变的 lst = [1,2,3,4] #改 lst[(元素下标 ...
- Linux开机以root账户自动登录
最近我们的自动化测试平台需要支持中标麒麟系统,对于我们来说要让这个系统支持分布式自动化测试,最重要的一点就是虚拟机启动后自动以root账户登录系统,并且执行我们的环境配置脚本,那么如何能让它开启自动登 ...
- Cookie、Session、JWT在koa中的应用及实现原理
目录 Cookie 重要属性 实现原理 cookie签名实现原理 注意事项 Session 实现原理 JWT 使用方式 组成 实际应用 实现原理 前端存储方式 cookie session local ...
- (原创)[.Net] 进程间通信框架(基于共享内存)——SimpleMMF
一.前言 进程间通信技术的应用非常广泛,在Windows下常用的实现方式有:管道.Socket.消息.本地文件.共享内存等,每种方式都有各自适应的场景. 在进行大数据交换时,最优的方式便是共享内存. ...
- Mybatis学习笔记-ResultMap结果集映射
解决属性名与字段名不一致的问题 新建项目 --> 测试实体类字段不一致的情况 数据库字段:id,name,pwd 实体类属性:id,name,password 输出结果 User{id=1, n ...
- Java中泛型的详细解析,深入分析泛型的使用方式
泛型的基本概念 泛型: 参数化类型 参数: 定义方法时有形参 调用方法时传递实参 参数化类型: 将类型由原来的具体的类型参数化,类似方法中的变量参数 类型定义成参数形式, 可以称为类型形参 在使用或者 ...
- for,while,until三种循环
目录 一.echo命令-改变输出字符串或者提取shell变量的值 1.1..格式 2.2.常用参数 二.for循环语句 2.1.for循环结构 示例1 示例2 三.while循环语句结构 示例1 示例 ...
- C++STL——vector类
vector容器 1.1 vector容器的基本概念 Array 是静态空间,一旦配置了就不能改变,要换大一点或者小一 点的空间,可以,一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的 数据搬 ...
- Ubuntu安装arm-linux-gcc 步骤
文章目录 1.获取压缩包 2.解压 压缩包到指定路径 3.新建目录,解放目录权限 4.复制到新目录 5.配置环境变量和库变量 6.使用source命令重新加载生效该配置文件 7.验证 gcc编译的只能 ...