运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get和 写入数据 put 。
- 获取数据
get(key)- 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。 - 写入数据
put(key, value)- 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶: 你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得关键字 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得关键字 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
该题要求使用数据结构来实现LRU的读和写,解题之前应该了解一下什么是LRU。之前在操作系统和Spring Boot中的缓存及实现原理源码解读 一文中已经对于LRU以及操作系统中其他的内存替换策略做了介绍,这里就不再赘述。简而言之,LRU每次都选择最少最近使用的数据进行替换。如下所示:
例如,当页面访问到第一个3时未命中,而此时缓存中已满。根据LRU的替换思想,选择最少最近未使用的进行替换,那么缓存中的0、1、2只有2使用次数最少,而且使用时间最早。
如果需要使用一种数据结构来实现LRU,那么我们需要考虑如下几个问题:
- 如果维护缓存中不同数据之间的访问记录?
- 如何维护页面访问的次数和时间来为替换时提供依据?
对于第一个问题来说,由于数据访问具有时序性,因此,我们可以使用双向链表的方式来维护数据之间的时序关系,越早访问的元素越靠近链表头部。对于第二个问题,我们需要根据访问的时间和次数对数据进行排列,并且要求时间复杂度为O(1),那么最好的选择就是哈希表。因此,整体上可以使用链表 + 哈希表的结构来实现LRU。而且,我们需要在O(1)的时间复杂度下完成数据的读和写,所以链表节点的应该包含数据的key和value两部分信息。另外,哈希表中只需要保存key,根据key就可以访问到链表中对应的value。
链表 + 哈希表的数据结构正好对应Java中的LinkedHashMap,当然也可以自定义链表和哈希表,并建立两者之间的映射关系。下面直接使用LinkedHashMap来作为解题的选型,并且也会使用Python语言来自定义链表和哈希表实现LRU。
Java解题代码:
class LRUCache {
// 缓存容量大小
private int capacity;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
// 初始化内存空间大小
this.capacity = capacity;
}
public int get(int key) {
// 首先判断给定的key是否存在
if(cache.containsKey(key)){
// 更改状态为最近使用
makeRecently(key);
// 直接从cache中取值
return cache.get(key);
}
return -1;
}
public void put(int key, int value) {
// 如果给定的key中存在值,则只需更新值
if(cache.containsKey(key)){
// 传入的value会覆盖掉旧的value
cache.put(key, value);
// 修改该key的状态为最近使用
makeRecently(key);
return;
}
// 如果此时缓存已满,则需要进行淘汰
if(cache.size() >= this.capacity){
// 最老的数据即cache的首元素
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 否则,直接保存到缓存中
cache.put(key, value);
}
public void makeRecently(int key){
int value = cache.get(key);
// 先移除在加到尾部
cache.remove(key);
cache.put(key, value);
}
}
Python解题代码[官方题解],这里使用了伪首部和伪尾部来设置链表的界限,并且将最近访问的元素作为链表的头部元素。如果改成了上面相同的逻辑,只需要修改更新链表插入元素的操作即可:
class DLinkedNode:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.pre = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.cache = dict()
self.capacity = capacity
self.size = 0
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key: int) -> int:
if key not in self.cache:
return -1
# 如果 key 存在,先通过哈希表定位,再移到头部
node = self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果 key 不存在,创建一个新的节点
node = DLinkedNode(key, value)
# 添加进哈希表
self.cache[key] = node
# 添加至双向链表的头部
self.addToHead(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除双向链表的尾部节点
removed = self.removeTail()
# 删除哈希表中对应的项
self.cache.pop(removed.key)
self.size -= 1
else:
# 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node = self.cache[key]
node.value = value
self.moveToHead(node)
def addToHead(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 moveToHead(self, node):
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
node = self.tail.prev
self.removeNode(node)
return node