要求
设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。
get和put函数的时间复杂度因为O(1)
技术栈
HashMap和双向链表=LinkedHashMap的链式版本
HashMap:存放缓存数据<Integer, BiNode>
双向链表:存放节点顺序<BiNode>
举例
缓存容量:2
依次进行以下操作:
1. put(1,1)
2. put(2,2)
3. get(1)
3. put(3,3)
4. put(4,4)
图1 初始化虚拟头节点和尾节点
put(1,1)
图2 插入1
put(2,2)
图3 插入2
get(1)
图4 获取key为1的值
由于要删除最近访问的值,因此将key为1的节点移到头节点
细节:先移除,再移动(这里要注意顺序,不然容易出问题,如果先移动到头节点,再移除会出错,如图5)。
图5 先移动后移除图
put(3,3)
图6 put(3,3)
由于此时缓存元素为2,再插入一个元素,超过缓存容量,所以需要移除尾节点。然后将插入的元素移动到头部,如图6所示。
put(4,4)
图7 put(4,4)
解释如4所示。
LRU缓存代码
class LRUCache {
int size; // 元素个数
int capacity; // 容量
BiNode head; // 头节点
BiNode tail; // 尾节点
Map<Integer, BiNode> cache; // 缓存
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
this.cache = new HashMap<>();
head = new BiNode();
tail = new BiNode();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
BiNode node = cache.get(key);
if (node == null) {
return -1; // 不存在该元素
} else {
removeNode(node); // 移除老元素
addToHead(node); // 放到头部
return node.value;
}
}
public void put(int key, int value) {
BiNode node = cache.get(key);
if(node == null){ // 不存在该元素
BiNode newNode = new BiNode(key, value);
addToHead(newNode); // 放到头部
cache.put(key, newNode); // 放入缓存
size++;
if(size > capacity){
int K = tail.pre.key; // 移除的key
cache.remove(K); // 从缓存移除
removeTailNode(); // 移除尾部节点
size--;
}
}else { // 存在该元素
BiNode newNode = new BiNode(key, value);
addToHead(newNode); // 放到头部
removeNode(node); // 移除老节点
cache.put(key, newNode); // 放入缓存
}
}
private void removeNode(BiNode node){
node.next.pre = node.pre;
node.pre.next = node.next;
}
private void removeTailNode(){
BiNode node = tail.pre;
removeNode(node);
node = null; // help GC
}
private void addToHead(BiNode node){
node.next = head.next;
node.next.pre = node;
node.pre = head;
head.next = node;
}
class BiNode{
BiNode pre; // 前向指针
BiNode next; // 后向指针
int key; // key
int value; // value
public BiNode(){}
public BiNode(int K, int V){
key = K;
value = V;
}
}
}