面试官:来了,老弟,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操作来着,也分为两种情况。

  1. 缓存中存在。把缓存中的该元素移动到缓存头部。并返回对应的value值。
  2. 缓存中不存在。直接返回-1。

综上所述:对于一个LRU缓存结构来说,主要需要支持以下三种操作。

  1. 查找一个元素。
  2. 在缓存末尾删除一个元素。
  3. 在缓存头部添加一个元素。

所以,我们最容易想到的就是使用一个链表来实现LRU缓存。

我们可以维护一个有序的单链表,越靠近链表尾部的结点是越早访问的。

当我们进行Set操作时,我们从链表头开始顺序遍历。遍历的结果有两种情况。

  1. 如果此数据之前就已经被缓存在链表中,我们遍历得到这个数据对应的结点,然后将其从这个位置移动到链表的头部。
  2. 如果此数据不在链表中,又会分为两种情况。如果此时缓存链表没有满,我们直接将该结点插入链表头部。如果此时缓存链表已经满了,我们从链表尾部删除一个结点,然后将新的数据结点插入到链表头部。

当我们进行Get操作时,我们从链表头开始顺序遍历。遍历的结果有两种情况。

  1. 如果此数据之前就已经被缓存在链表中,我们遍历得到这个数据对应的结点,然后将其从这个位置移动到链表的头部。
  2. 如果此数据之前不在缓存中,我们直接返回-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?你学会了吗?的更多相关文章

  1. 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU

    你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...

  2. 阿里面试官让我实现一个线程安全并且可以设置过期时间的LRU缓存,我蒙了!

    目录 1. LRU 缓存介绍 2. ConcurrentLinkedQueue简单介绍 3. ReadWriteLock简单介绍 4.ScheduledExecutorService 简单介绍 5. ...

  3. Spring第三天,详解Bean的生命周期,学会后让面试官无话可说!

    点击下方链接回顾往期 不要再说不会Spring了!Spring第一天,学会进大厂! Spring第二天,你必须知道容器注册组件的几种方式!学废它吊打面试官! 今天讲解Spring中Bean的生命周期. ...

  4. 面试官的七种武器:Java篇

    起源 自己经历过的面试也不少了,互联网的.外企的,都有.总结一下这些面试的经验,发现面试官问的问题其实不外乎几个大类,玩不出太多新鲜玩意的.细细想来,面试官拥有以下七种武器.恰似古龙先生笔下的武侠世界 ...

  5. 走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串

    原文:走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串 对大量数据进行查询时,可以应用到索引技术.索引是一种特殊类型的数据库对象,它保存着数据表中一列或者多列的排序结果,有 ...

  6. 金三银四,如何征服面试官,拿到Offer

    又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往年「冷」一些,形式更加严峻一些,大家多多少少可能都听到或看到一些信息,就是好多公司在优化裁 ...

  7. 如何征服面试官,拿到Offer [转]

    转自 https://my.oschina.net/cccyb/blog/3012768 又到了茶余饭后的时间,想想写点什么,掐指一算,噢呦,快到3月份了,职场的金三银四跳槽季又来了,不同的是今年比往 ...

  8. 面试官:你了解过Redis对象底层实现吗

    上一章我们讲了Redis的底层数据结构,不了解的人可能会有疑问:这个和平时用的五大对象有啥关系呢?这一章我们就主要解释他们所建立的联系. 看这个文件之前,如果对ziplist.skiplist.int ...

  9. 《吊打面试官》系列-Redis常见面试题(带答案)

    你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源,有面试点思维导图,欢迎[Star]和[完善] 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在 ...

随机推荐

  1. python errno库与socket.connect_ex()方法的结合使用

    前言:一般socket链接会首选connect方法,该方法会一直尝试链接.那么今天展示下connect_ex()方法,该方法如果链接成功会返回0,失败会返回errno库中的errorcode中的key ...

  2. GoldenEye-v1靶机

    仅供个人娱乐 靶机信息 下载地址:https://pan.baidu.com/s/1dzs_qx-YwYHk-vanbUeIxQ 一.主机扫描 二.信息收集 三.漏洞的查找和利用 boris    I ...

  3. DC-3靶机

    仅供个人娱乐 靶机信息 下载地址:http://www.five86.com/downloads/DC-3.zip 一.主机扫描 arp-scan -l nmap -A -p 1-65535 -sV ...

  4. 搭建SSM基础环境>基于idea

    目录 搭建SSM基础环境 创建一个Web项目 导入所需要的jar包 在项目目录下创建一个Resources文件夹并设置为类路径 在src目录下创建项目的初始文件夹目录 在resources文件夹下创建 ...

  5. CentOS 7 安装虚拟机

    1.本次安装centos7 安装使用的软件是VitrualBox 虚拟机软件 Oracle公司的虚拟机软件,免费商品(大家可以百度搜索去官网下载) 1:我这里使用的是阿里的centos7的镜像(大家可 ...

  6. OpenFaaS实战之八:自制模板(maven+jdk8)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  7. 通读BadCode

    简介 就是旋哥的BadCode系列,这次好好通读下,然后我在旋哥的注释上又加了一些,函数原型等. 项目地址:https://github.com/Rvn0xsy/BadCode 第一课 主要介绍了下c ...

  8. 数据结构与算法-排序(六)堆排序(Heap Sort)

    摘要 堆排序需要用到一种数据结构,大顶堆.大顶堆是一种二叉树结构,本质是父节点的数大于它的左右子节点的数,左右子节点的大小顺序不限制,也就是根节点是最大的值. 这里就是不断的将大顶堆的根节点的元素和尾 ...

  9. GIT·全局配置文件及项目配置文件

    阅文时长 | 0.03分钟 字数统计 | 48.8字符 主要内容 | 1.引言&背景 2.声明与参考资料 『GIT·全局配置文件及项目配置文件』 编写人 | SCscHero 编写时间 | 2 ...

  10. git的基本操作命令与基础

    本文针对window用户进行git操作 1.git是分布式版本控制系统,需要用户名和邮箱作为一个标识.在任何文件夹中点击右键,选择Git Bash Here ,配置全局用户名和邮件或者局部用户名和邮件 ...