面试官:来了,老弟,LRU缓存实现一下?

我:直接LinkedHashMap就好了。

面试官:不要用现有的实现,自己实现一个。

我:.....

面试官:回去等消息吧....


大家好,我是程序员学长,今天我们来聊一聊LRU缓存问题。

Tips: LRU在计算机软件中无处不在,希望大家一定要了解透彻。

问题描述

  1. 设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
  2. 1. set(key, value):将记录(key, value)插入该结构
  3. 2. get(key):返回key对应的value

分析问题

根据问题描述,我们可以知道LRU包含两种操作,即Set和Get操作。

对于Set操作来说,分为两种情况。

  1. 1. 缓存中已经存在。把缓存中的该元素移动到缓存头部。
  2. 2. 如果缓存中不存在。把该元素添加到缓存头部。如果此时缓存的大小超过限制的大小,需要删除缓存中末尾的元素。

对于Get操作来着,也分为两种情况。

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

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

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

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

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

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

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

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

  1. 如果此数据之前就已经被缓存在链表中,我们遍历得到这个数据对应的结点,然后将其从这个位置移动到链表的头部。
  2. 如果此数据之前不在缓存中,我们直接返回-1。

下面我们来看一下代码如何实现。

  1. class LinkedNode:
  2. def __init__(self, key=0, value=0):
  3. self.key = key
  4. self.value = value
  5. self.next = None
  6. class LRUCache():
  7. def __init__(self, capacity: int):
  8. # 使用伪头部节点
  9. self.capacity=capacity
  10. self.head = LinkedNode()
  11. self.head.next=None
  12. self.size = 0
  13. def get(self, key: int) -> int:
  14. cur=self.head.next
  15. pre=self.head
  16. while cur!=None:
  17. if cur.key==key:
  18. pre.next = cur.next
  19. cur.next = self.head.next
  20. self.head.next = cur
  21. break
  22. pre=pre.next
  23. cur=cur.next
  24. if cur!=None:
  25. return cur.value
  26. else:
  27. return -1
  28. def put(self, key: int, value: int) -> None:
  29. cur = self.head.next
  30. pre = self.head
  31. #缓存没有元素,直接添加
  32. if cur==None:
  33. node = LinkedNode()
  34. node.key = key
  35. node.value = value
  36. self.head.next = node
  37. self.size = self.size + 1
  38. return
  39. #缓存有元素,判断是否存在于缓存中
  40. while cur!=None:
  41. #表示已经存在
  42. if cur.key == key:
  43. #把该元素反正链表头部
  44. cur.value=value
  45. pre.next = cur.next
  46. cur.next = self.head.next
  47. self.head.next = cur
  48. break
  49. #代表当前元素时最后一个元素
  50. if cur.next==None:
  51. #如果此时缓存已经满了,淘汰最后一个元素
  52. if self.size==self.capacity:
  53. pre.next=None
  54. self.size=self.size-1
  55. node=LinkedNode()
  56. node.key=key
  57. node.value=value
  58. node.next=self.head.next
  59. self.head.next=node
  60. self.size=self.size+1
  61. break
  62. pre = pre.next
  63. cur=cur.next

这样我们就用链表实现了一个LRU缓存,我们接下来分析一下缓存访问的时间复杂度。对于Set来说,不管缓存有没有满,我们都需要遍历一遍链表,所以时间复杂度是O(n)。对于Get操作来说,也是需要遍历一遍链表,所以时间复杂度也是O(n)。

优化

​ 从上面的分析,我们可以看到。如果用单链表来实现LRU,不论是Set还是Get操作,都需要遍历一遍链表,来查找当前元素是否在缓存中,时间复杂度为O(n),那我们可以优化吗?我们知道,使用hash表,我们查找元素的时间复杂度可以减低到O(1),如果我们可以用hash表,来替代上述的查找操作,那不就可以减低时间复杂度吗?根据这个逻辑,所以我们采用hash表和链表的组合方式来实现一个高效的LRU缓存。

  1. class LinkedNode:
  2. def __init__(self, key=0, value=0):
  3. self.key = key
  4. self.value = value
  5. self.prev = None
  6. self.next = None
  7. class LRUCache:
  8. def __init__(self, capacity: int):
  9. self.cache = dict()
  10. self.head = LinkedNode()
  11. self.tail = LinkedNode()
  12. self.head.next = self.tail
  13. self.tail.prev = self.head
  14. self.capacity = capacity
  15. self.size = 0
  16. def get(self, key: int):
  17. #如果key不存在,直接返回-1
  18. if key not in self.cache:
  19. return -1
  20. #通过hash表定位位置,然后删除,省去遍历查找过程
  21. node = self.cache[key]
  22. self.moveHead(node)
  23. return node.value
  24. def put(self, key: int, value: int) -> None:
  25. if key not in self.cache:
  26. # 如果key不存在,创建一个新的节点
  27. node = LinkedNode(key, value)
  28. # 添加进哈希表
  29. self.cache[key] = node
  30. self.addHead(node)
  31. self.size += 1
  32. if self.size > self.capacity:
  33. # 如果超出容量,删除双向链表的尾部节点
  34. removed = self.removeTail()
  35. # 删除哈希表中对应的项
  36. self.cache.pop(removed.key)
  37. self.size -= 1
  38. else:
  39. node = self.cache[key]
  40. node.value = value
  41. self.moveHead(node)
  42. def addHead(self, node):
  43. node.prev = self.head
  44. node.next = self.head.next
  45. self.head.next.prev = node
  46. self.head.next = node
  47. def removeNode(self, node):
  48. node.prev.next = node.next
  49. node.next.prev = node.prev
  50. def moveHead(self, node):
  51. self.removeNode(node)
  52. self.addHead(node)
  53. def removeTail(self):
  54. node = self.tail.prev
  55. self.removeNode(node)
  56. return node

总结

LRU缓存不论在工作中还是面试中,我们都会经常碰到。希望这篇文章能对你有所帮助。

今天,我们就聊到这里。更多有趣知识,请关注公众号【程序员学长】。我给你准备了上百本学习资料,包括python、java、数据结构和算法等。如果需要,请关注公众号【程序员学长】,回复【资料】,即可得。

你知道的越多,你的思维也就越开阔,我们下期再见。

手撕LRU缓存了解一下的更多相关文章

  1. 手撕LRU缓存

    面试官:来了,老弟,LRU缓存实现一下? 我:直接LinkedHashMap就好了. 面试官:不要用现有的实现,自己实现一个. 我:..... 面试官:回去等消息吧.... 大家好,我是程序员学长,今 ...

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

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

  3. HashMap+双向链表手写LRU缓存算法/页面置换算法

    import java.util.Hashtable; class DLinkedList { String key; //键 int value; //值 DLinkedList pre; //双向 ...

  4. Java:手写幼儿园级线程安全LRU缓存X探究影响命中率的因素

    最近遇到一个需求,需要频繁访问数据库,但是访问的内容只是 id + 名称 这样的简单键值对. 频繁的访问数据库,网络上和内存上都会给数据库服务器带来不小负担. 于是打算写一个简单的LRU缓存来缓存这样 ...

  5. Netty实现高性能IOT服务器(Groza)之手撕MQTT协议篇上

    前言 诞生及优势 MQTT由Andy Stanford-Clark(IBM)和Arlen Nipper(Eurotech,现为Cirrus Link)于1999年开发,用于监测穿越沙漠的石油管道.目标 ...

  6. 面试挂在了 LRU 缓存算法设计上

    好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存 ...

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

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

  8. LRU缓存实现(Java)

    LRU Cache的LinkedHashMap实现 LRU Cache的链表+HashMap实现 LinkedHashMap的FIFO实现 调用示例 LRU是Least Recently Used 的 ...

  9. 转: LRU缓存介绍与实现 (Java)

    引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本, ...

随机推荐

  1. Ubuntu 19.10安装Wine软件

    ======================================== 我使用的操作系统版本为Ubuntu 19.10 64位,如果是32位Ubuntu19.10则可以跳过步骤一 1.添加 ...

  2. [SqlServer] 理解数据库中的数据页结构

    这边文章,我将会带你深入分析数据库中 数据页 的结构.通过这篇文章的学习,你将掌握如下知识点: 1. 查看一个 表/索引 占用了多少了页. 2. 查看某一页中存储了什么的数据. 3. 验证在数据库中用 ...

  3. 使用JavaMailSender 发送邮件

    使用JavaMailSender 发送邮件 package com.juvenxu.mvnbook.account.email; import javax.mail.MessagingExceptio ...

  4. MERCY靶机

    仅供个人娱乐 靶机信息 下载地址:https://drive.google.com/uc?id=1YzsW1lCKjo_WEr6Pk511DXQBFyMMR14y&export=downloa ...

  5. 15 道超经典大厂 Java 面试题!重中之重

    从超高频的后端面试题出发,指明学习方向 大家好,我是鱼皮. 还记得我的老弟小阿巴么?他目前正值大一暑假,在家自学编程(刷短视频)中. 他整个大一期间基本都在学习前端.后来,我带他写了一次后端,结果就崩 ...

  6. (数据科学学习手札126)Python中JSON结构数据的高效增删改操作

    本文示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 在上一期文章中我们一起学习了在Python ...

  7. jmeter之JDBC类组件

    ~什么是JDBC?:全称名为Java DataBase Connectivity,(java数据库连接),在jmeter中是一种可以远程操作数据库的一类组件. ~jmeter如何操作数据库?:jmet ...

  8. 跟我一起写 Makefile(十一)

    make 的运行 ------ 一般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的.但也有时你也许只想让make重编译某些文件,而不 ...

  9. React Native 启动流程简析

    导读:本文以 react-native-cli 创建的示例工程(安卓部分)为例,分析 React Native 的启动流程. 工程创建步骤可以参考官网.本文所分析 React Native 版本为 v ...

  10. 自己动手实现Lua--实现TAILCALL指令

    最近在看<自己动手实现Lua-虚拟机.编译器和标准库>.这是本挺不错的书,通过学习此书能够对Lua语言有比较深刻的理解,此外还可以对如何自己实现一门脚本语言有直观的认识.对于想学习Lua的 ...