题目传送门:146. LRU 缓存 - 力扣(LeetCode)
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
LRU基于局部性原理:若数据近期被访问,未来被再次访问的概率较高。当缓存空间不足时,算法会淘汰最久未被使用的数据,如:
- 缓存容量为3,依次访问数据A、B、C后缓存为[C, B, A](A最久未用)
- 再次访问B时,缓存更新为[B, C, A]
- 若新加入D,则淘汰A,缓存变为[D, B, C]
为了实现O(1)时间复杂度的get/put操作,LRU常结合哈希表+双向链表:
- 哈希表:快速定位键值对(O(1)查找),通过缓存数据的键映射到其在双向链表中的位置。存储的key就是用户查询的键,值为指向双向链表节点的指针/引用。
- 双向链表:维护访问顺序,头部为最新访问节点,尾部为待淘汰节点(最久未使用)。
这样,我们首先用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在O(1)时间复杂度完成get/put操作:
- get(key):首先判断key是否存在:
- 如果key不存在,则返回-1。
- 如果key存在,则key对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
- put(key, value):首先判断key是否存在:
- 如果key不存在,使用key和value创建一个新的节点,在双向链表的头部添加该节点,并将key和该节点添加到哈希表中。然后判断双向链表是否超出容量,如果超出容量,则删除双向链表尾部节点,并删除哈希表中对应的项。
- 如果key存在,则与get操作类似,先通过哈希表定位,再将对应的节点的值更新为value,并将该节点移动到双向链表的头部。
- 小贴士:在双向链表的实现中,使用一个伪头部和伪尾部标记界限,这样在添加和删除节点时不需要检测相邻节点是否存在。
class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
++size;
if (size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
--size;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}