面试官:来了,老弟,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. 缓存中已经存在。把缓存中的该元素移动到缓存头部。
  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、数据结构和算法等。如果需要,请关注公众号【程序员学长】,回复【资料】,即可得。

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

版权声明:本文为laohanshuibi原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/laohanshuibi/p/15225912.html