题目链接
https://leetcode.com/problems/lru-cache/
题目描述
运用你所掌握的数据结构,设计并实现一个LRU(Least Recently Used 最近最少使用)缓存机制。计算机的缓存容量是有限的,如果缓存满了就要删除很久没用过的内容(认为很久没用过的数据是无用的),给新内容腾位置。
实现LRUCache类:
(1)以一个正整数参数capacity作为容量初始化LRU缓存
(2)实现get(key),如果该key存在于缓存中,则返回该key对应的value,否则返回-1。
(3)实现put(key,value),如果该key已存在,则变更对应的value。如果key不存在,则直接将key-value键值对插入。如果插入新数据前缓存已满,则应该在写入新数据之前删除最久未使用的键值对,为新数据留出空间。
get(key)和put(key,value)的时间复杂度为O(1)。
示例
输入:
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"][[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, null, -1, 3, 4]解析:
LRUCache lRUCache = new LRUCache(2); #缓存容量为2。可以将Cache理解为一个队列,假定把最近使用的放到队尾,很久没用的排在队头。键值对表示为(key,value)
lRUCache.put(1, 1); // 缓存是 [(1,1)]
lRUCache.put(2, 2); // 缓存是 [(1,1),(2,2)] 键值对(2,2)插入到队尾
lRUCache.get(1); // 返回 1。由于最近访问了key=1,因此将(1,1)调整到队尾,缓存为 [(2,2),(1,1)]
lRUCache.put(3, 3); // 缓存容量已满,插入前需要删除队头元素即很久没用的元素,然后将新数据插入到队尾。缓存是 [ (1,1),(3,3)]
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 缓存容量已满,需要将(1,1)删除,然后将新数据插入到队尾,缓存是 [(3,3),(4,4)]
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3,key=3被访问,因此将键值对放到队尾 [(4,4),(3,3)]
lRUCache.get(4); // 返回 4,key=4被访问,因此将键值对放到队尾[(3,3),(4,4)]
解题思路
Cache选用的数据结构需要具备以下几个条件:
(1)元素顺序要能区分最近使用的数据和很久没用的数据。
(2)get的时间复杂度为O(1)。能够快速确认某个key是否存在,并且得到对应的value。
(3)每次访问cache中的某个key时(查询、插入),需要将该元素的顺序进行调整,以表示“最近使用的数据”。同时当缓存已满且需要新增元素时,需要将最久未使用的元素删除,并将新元素插入并表示为“最近使用的数据”。因此cache必须也要支持在任意位置快速插入、删除数据。
和单向链表不同的是,双向链表在删除元素时,不需要再遍历找该元素的前序节点。单链表和双向链表插入、删除元素的时间复杂度为:
(1)插入、删除链表头部元素:单向链表O(1),双向链表O(1)。
(2)插入、删除链表尾部元素:单向链表O(n),双向链表O(1)。
(3)平均:单向链表O(n),双向链表O(n)。
因此双向链表在头部和尾部的删除、插入操作时间复杂度为O(1)。但要实现任意位置O(1)时间复杂度的删除,需要再借助哈希表快速定位到链表中的节点。
因此LRU缓存算法用到的数据结构为哈希双向链表。用一个哈希表和一个双向链表来维护缓存空间中的键值对。其中链表中的一个节点包含(key,value)两个元素,哈希表中某个关键字对应的value是具有相同关键字的链表节点。规定最近使用的元素靠近链表尾部,最久未使用的元素靠近链表头部。
下面依次介绍LRUCache对象的创建以及get、put函数的实现。
创建LRUCache对象
LRUCache要保存一个哈希表以及双向链表(在双向链表的实现中,要使用伪头部(dummy head)和伪尾部(dummy tail)来标识链表的头部和尾部。如果不保存这两个节点,在向链表中插入一个元素时还需要判断插入位置是否在链表的头部或尾部)。同时传入的capacity参数也要作为LRUCache的一个属性。
实现get方法
get方法传入参数key,查找哈希表中的关键字是否包含key,如果不包含,则返回-1。 如果包含,则根据key查找哈希表中对应的value即链表节点,并返回该节点存放的value。另外,还要把被访问节点的位置挪到链表尾部,以表示最近被使用。
实现put方法
put方法传入参数key和value。如果key在哈希表中已经存在,根据key找到对应的链表节点,修改value为新传入的值,并将该节点移动到链表尾部,然后更新哈希表中key指向的node。(具体实现时,可以先将关键字为key的链表节点和哈希表元素删除,根据传入的key和value创建新节点new_node并插入链表尾部,并将(key,new node)加入哈希表中。)
如果key在哈希表中不存在,则需要判断链表容量是否已满。如果链表满了,则删除链表头部的Node,根据该node存的key删除对应的哈希表元素,将新元素加入链表尾部,并在哈希表中增加(key,node)键值对。如果链表未满,则直接将新元素加入链表尾部,并且在哈希表中增加(key,node)键值对。
需要记住的是,在插入、删除链表的时候也要记得更新哈希表。
Python代码实现
在Python中字典的底层数据结构是哈希表,插入、删除、查找的时间复杂度都为O(1)。
LRUCache初始化
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity #最大容量
self.cache = DoubleLinkedList() #双向链表实例
self.dic= dict{} #哈希表
双向链表结构实现,要实现三个函数:
将链表头部元素删除并返回该元素:removeHead(),
将给定的节点从双链表中删除removeNode(node),
将node加入双链表尾部:addTail(node)。
class DoubleList:
def __init__(self):
self.head = Node(0,0)
self.tail = Node(0,0)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0 #链表长度
def removeHead(self): #删除链表头部元素并返回该元素
if(self.head.next == self.tail):
return None
firstNode = self.head.next
self.removeNode(firstNode)
return firstNode
def removeNode(self,node): #删除给定的node
node.next.prev = node.prev
node.prev.next = node.next
self.size -= 1
def addTail(self,node): #在链表尾部增加node
node.next = self.tail
node.prev = self.tail.prev
node.prev.next = node
self.tail.prev = node
self.size += 1
我们在LRUCache数据结构和双链表、字典数据结构之间构建一层抽象api。在LRUCache的get和put函数中不直接操作链表和字典。我们创建四个操作函数:将链表已有元素调整为最近使用的元素、新增最近使用的元素、删除最久未使用的元素、根据key从链表和哈希表中删除元素。
操作函数
#将新元素加入到链表中
def addMostRecently(self,key,value):
node = Node(key,value)
self.cache.addTail(node)
self.dic[node.key] = node
#将最久未被使用的元素删除。
def removeLeastRecently(self):
removedHead = self.cache.removeHead() #从链表中删除并得到该节点
if removedHead:
self.dic.pop(removedHead.key)#根据该节点的key在哈希表中删除对应元素
#根据key得到待删除的链表节点,从双链表中删除该节点并在哈希表中删除对应元素
def remove(self,key):
node = self.dic[key]
self.cache.removeNode(node)
self.dic.pop(key)
#将链表中某个节点提升为“最近使用”的状态
def adjustMostRecently(self,key):
node = self.dic[key]
self.cache.removeNode(node)
self.cache.addTail(node)
LRUCache get(key)、put(key,value)方法
def get(self, key: int) -> int:
if key in self.dic:
self.adjustMostRecently(key)
return self.dic[key].val
else:
return -1
def put(self, key: int, value: int) -> None:
if(key in self.dic):#首先判断key在哈希表中是否存在,如果存在,先把该key从哈希表和链表中去除
self.remove(key)
elif(self.cache.size == self.capacity):#如果该key不存在,需要判断链表是否满了
self.removeLeastRecently() #如果满了,去除最久不用的元素
self.addMostRecently(key,value) #加入新元素
整体代码
class Node:
def __init__(self,key,val):
self.prev = None #指向前面元素
self.next = None #指向后面元素
self.key = key
self.val = val
class DoubleList:
def __init__(self):
self.head = Node(0,0)
self.tail = Node(0,0)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0 #链表长度
def removeHead(self): #删除链表头部元素并返回该元素
if(self.head.next == self.tail):
return None
firstNode = self.head.next
self.removeNode(firstNode)
return firstNode
def removeNode(self,node):
node.next.prev = node.prev
node.prev.next = node.next
self.size -= 1
def addTail(self,node):
node.next = self.tail
node.prev = self.tail.prev
node.prev.next = node
self.tail.prev = node
self.size += 1
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = DoubleList()
self.dic = {}
def addMostRecently(self,key,value):
node = Node(key,value)
self.cache.addTail(node)
self.dic[node.key] = node
def removeLeastRecently(self):
removedHead = self.cache.removeHead()
if removedHead:
self.dic.pop(removedHead.key)
def remove(self,key):
node = self.dic[key]
self.cache.removeNode(node)
self.dic.pop(key)
def adjustMostRecently(self,key):
node = self.dic[key]
self.cache.removeNode(node)
self.cache.addTail(node)
def get(self, key: int) -> int:
if key in self.dic:
self.adjustMostRecently(key)
return self.dic[key].val
else:
return -1
def put(self, key: int, value: int) -> None:
if(key in self.dic):
self.remove(key)
elif(self.cache.size == self.capacity):
self.removeLeastRecently()
self.addMostRecently(key,value)