题目描述
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
示例
输入
["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);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
思路分析
题目描述中时让用自己掌握的数据结构设计实现一个LRU缓存机制,我第一反应是使用哈希表,通过对哈希表进行增删查改等操作完成LRU缓存机制的 ’ put,get '方法,刚开始想的是再定义一个哈希表用来存储各个元素被操作的次数,来判断该删除哪个元素,但是后来发现不行,这样还是不能表征缓存机制里元素里的使用情况(顺序问题),既然涉及到顺序问题,那么在数据结构中最能反映顺序的应该是链表了,在此处使用的是双向链表,但是我总觉得单向链表应该也可以吧,但是没有尝试,大家有兴趣的可以尝试一下,我在评论区等你们。下边说一下解题思路的大致逻辑。
初始化方法中定义一个带两个虚拟节点的双向链表和一个哈希表,并且明确LRU缓存的容量,以及当前缓存大小。
接下来便是添加元素,如果当前缓存中包含该元素,则只需要找出该元素,并改变其值就好了;
如果当前缓存中不包含该元素,先判断当前缓存是否已经到了上限,如果没到上限,那么直接添加,并把该节点移动到双向链表的最前端(在这里将最近操作的元素放在双向链表的开头位置,这样许久未操作的元素会排在双向链表的尾部);如果到了上限,则需删掉双向链表的尾部元素,并在缓存中删掉,然后添加新元素,并把新元素移动到双向链表的头部位置。
最后是取元素的方法,如果待取元素不在缓存中,直接返回 -1;如果待取元素在缓存中,则直接返回该元素的值。
注意:在这道题目中比较巧妙的就是使用双向链表+哈希表,双向链表的每个节点存储元素,双向链表的顺序表示了当前元素被操作的顺序情况(最近被操作的元素放在头部位置,最近没被操作过的放在尾部),方便我们删除当前最少被使用的元素(双向链表尾部元素)。缓存cache中则保留了key和value的映射关系。
思路和逻辑先讲到这,我们看一下代码的具体实现:
代码
# 双向链表的类定义
class dual_linkList_node:
def __init__(self, key=0, val=0):
self.key = key
self.val = val
self.prev = None
self.next = None
# LRU缓存机制的类定义
class LRUCache:
# LRU缓存机制的初始化方法
def __init__(self, capacity: int):
self.cache = dict() # 初始化存储映射关系的字典
self.head = dual_linkList_node() # 双向链表的头节点
self.tail = dual_linkList_node() # 双向链表的尾节点
self.head.next = self.tail # 头节点指向尾节点
self.tail.prev = self.head # 尾节点指向头节点, 至此双向链表构建完成
self.capacity = capacity # 初始化LRUCache的容量大小
self.size = 0
# 在这里将元素储存在双向链表中, 映射关系存在字典里 #
def get(self, key: int) -> int: # 获取LRUCache中的某个元素
if key in self.cache: # 若待取元素在cache内, 则直接根据key取出该节点, 再将该节点移动到双向链表的头部, 并返回该元素的值
node = self.cache[key]
self.move_to_head(node)
return node.val
else: # 如果待取元素不在cache内, 返回 -1
return -1
# 删除双向链表的一个元素
def remove_node(self, node):
node.prev.next = node.next # node的前一个节点向后指向node后一个节点
node.next.prev = node.prev # node的后一个节点向前指向node前一个节点
# 在head节点之后添加node
def add_to_head(self, node):
# 在双向链表中, node和self.head都需要进行变换
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
# node节点移动到双向链表头节点之后
def move_to_head(self, node):
self.remove_node(node) # 先在双向链表中移除该node节点
self.add_to_head(node) # 再将该node节点添加在head之后
# 移除双向链表尾节点
def remove_tail(self):
# 因为头节点和尾节点都为虚拟节点,因此删除尾节点即删除虚拟尾节点的前一个节点
node = self.tail.prev
self.remove_node(node)
return node
# 添加元素
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果 key 不存在,创建一个新的节点
node = dual_linkList_node(key, value)
# 添加进哈希表
self.cache[key] = node
# 添加至双向链表的头部
self.add_to_head(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除双向链表的尾部节点
removed = self.remove_tail()
# 删除哈希表中对应的项
self.cache.pop(removed.key)
self.size -= 1
else:
# 如果带输入的元素存在于cache,则只需取出存储该元素的节点,改变其值并移动到双向链表头节点的位置即可
node = self.cache[key]
node.val = value
self.move_to_head(node)
运行结果