最近最少使用缓存
英文名字为least recently used cache
设计和构建一个“最近最少使用”缓存:
- 该缓存会删除最近最少使用的项目;
- 缓存应该从键映射到值(允许你插入和检索特定键对应的值);
- 并在初始化时指定最大容量;
- 当缓存被填满时,它应该删除最近最少使用的项目。
- 它应该支持以下操作:获取数据 get 和 写入数据 put
接口描述
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
实现思路
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
- 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
- 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:
- 对于 get 操作,首先判断 key 是否存在:
- 如果 key 不存在,则返回 −1;
- 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
- 对于 put 操作,首先判断 key 是否存在:
- 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
- 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。
提示
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
代码(Swift)
class LRUCache {
static let notFound = -1
private let capacity: Int
private var cache: [Int: Node]
private let head = Node(key: 0, value: 0)
private let tail = Node(key: 0, value: 0)
init(_ capacity: Int) {
self.capacity = capacity
cache = [Int: Node](minimumCapacity: capacity)
head.next = tail
tail.prev = head
}
func get(_ key: Int) -> Int {
//查看缓存
if let node = cache[key] {
//节点前移
moveNodeToHead(node)
return node.value
}
return LRUCache.notFound
}
func put(_ key: Int, _ value: Int) {
//查看缓存
if let node = cache[key] {
//节点更新和前移
node.value = value
moveNodeToHead(node)
return
}
//查看是否超容量
if cache.count >= capacity {
let last = removeTail()
cache.removeValue(forKey: last.key)
}
//添加节点
let node = Node(key: key, value: value)
cache[key] = node
addNodeToHead(node)
}
func removeTail() -> Node {
let last = tail.prev!
removeNode(last)
return last
}
func removeNode(_ node: Node) {
node.prev?.next = node.next
node.next?.prev = node.prev
}
func addNodeToHead(_ node: Node) {
node.next = head.next
head.next?.prev = node
node.prev = head
head.next = node
}
func moveNodeToHead(_ node: Node) {
removeNode(node)
addNodeToHead(node)
}
final class Node {
var key: Int
var value: Int
var prev: Node?
var next: Node?
init(key: Int, value: Int) {
self.key = key
self.value = value
}
}
}