运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int 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);
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
提示:
1 <= capacity <= 3000
0 <= key <= 3000
0 <= value <= 104
最多调用 3 * 104 次 get 和 put
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lru-cache
LRUCache需要满足:如果在添加元素的时候容量不够,那么将最久没有访问的节点删除掉。
那么其实内部需要维护的逻辑就是,在插入或者查询元素的时候,需要将频繁操作的元素的生命周期延长,那么就可以将经常操作元素放置在头部(或者尾部,自己统一逻辑就好),那么尾部的元素自然就是最久没有访问的了。当每次容量不够的时候,就将尾部的元素移除掉即可。
那么就涉及到两个要素:
1 元素需要按照操作顺序排序
2 查找元素要足够快,删除元素也要快。
Map速度查询快,链表是有序的,双向链表删除快,结合这两点就可以形成如下结构:
HashMap中再维护一个双向链表,黑色的节点是存储在HashMap中的,红色的线表示的是双向链表的方向。其实也就是在原有的HashMap的基础上,增加了元素之间的关系,可以按照特定顺序去访问。其实这就是LinkedHashMap的内部结构。
那么为什么不用单链表呢?因为在删除的时候是需要获取到上一个节点的,如果是单链表的话就需要将链表从头到尾再访问一遍。
本题具体实现如下:
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
class LRUCache {
private LRUCache.Node dummyHead;
private LRUCache.Node dummyTail;
private Map<Integer, Node> map;
int capacity = 0;
class Node {
Node pre;
Node next;
Integer key;
Integer value;
public Node(Integer key, Integer value) {
this.key = key;
this.value = value;
}
}
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(capacity);
dummyHead = new Node(-1, -1);
dummyTail = new Node(-1, -1);
dummyHead.next = dummyTail;
dummyTail.pre = dummyHead;
}
public int get(int key) {
if (map.containsKey(key)) {
Node node = map.get(key);
moveToHead(node);
return node.value;
} else {
return -1;
}
}
private void moveToHead(Node node) {
// pre -> node -> next
node.pre.next = node.next;
node.next.pre = node.pre;
addToHead(node);
}
private void addToHead(Node node) {
//dummyHead -> node -> oldHead
Node oldeHead = dummyHead.next;
oldeHead.pre = node;
node.next = oldeHead;
node.pre = dummyHead;
dummyHead.next = node;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
moveToHead(node);
return;
}
if (map.size() >= capacity) {
Node tailNode = removeTailNode();
map.remove(tailNode.key);
}
Node node = new Node(key, value);
map.put(key, node);
addToHead(node);
}
private Node removeTailNode() {
//pre -> node -> dummyTail
Node oldNode = dummyTail.pre;
Node newNode = oldNode.pre;
newNode.next = dummyTail;
dummyTail.pre = newNode;
oldNode.pre = null;
oldNode.next = null;
return oldNode;
}
}