Least recently used (LRU),中文翻译过来是最近最少使用,是一种容量受限的缓存策略。当新加入一个键时,如果缓存容器已经满了,那么该建将替换最久没有被访问的元素。因此缓存容器内都是最近被访问的元素,这也是符合常理的,一个最近被访问的健,未来一段时间内再次被访问的概率应该是较大的。
LRU缓存的设计在LeetCode上也有对应的题目:https://leetcode.com/problems/lru-cache/description/。一个最基本的LRU缓存应该满足以下一个操作:1、支持指定缓冲器的容量;2、支持put操作(向缓存器中添加元素);3、支持get操作(访问缓存器中的元素)。从调用者的角度来看,调用者的使用可能如下:
LRUCache cache = new LRUCache( 2 /* capacity */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // returns 1
cache.put(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.put(4, 4); // evicts key 1
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4
下面给出两种LRU的具体实现:
方案一:继承LInkedHashMap。
在这篇博客中,我们分析了LinkedHashMap的源码,我们已经知道了LInkedHashMap的底层实现原理,其在HashMap的基础上增加了双向链表来维持元素的插入顺序或者访问顺序。并且我们也谈到了,这种设计思路非常的妙,它使双向链表和HashMap共用了key和value。当LinkedHashMap维访问顺序时,每次的get/put操作它都会讲结点移到双向链表的尾部。并且它还提供了一个函数:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
用来判断是否删除最久没有被访问到的元素,默认的实现是一直放回false,也就是一直不删除最久没被访问到的元素。我们可以重写该方法来实现我们自己的逻辑:比如当容器容量大于某个值时就删除最久没被访问到的元素。
基于这个思路,我们就可以实现第一种方案了:
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity; // 重写父类方法,当容器容量大于指定大小时就删除最久没被访问的元素
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
}
方案二:使用HashMap+双向链表来实现:
这种方案和上一种是一样的,这两种方案的对比,就更能知道LinkedHashMap中双向链表和hash表共用key,value的区别了。
HashMap保存的是<key,Node>之间的映射,这样知道了key马上就可以找到其对应的结点Node,就可以知道其对应的值了。另外在双向链表的设计上,添加了伪头结点与伪尾结点,避免增加,删除元素时繁琐的判断。还有一点与LinkedHashMap中的区别就是最近访问的元素会被放在双向链表的头部而不是尾部。
class LRUCache {
static class Node { // 双向链表的设计,可以看到双向链表里面也保存了一份key,value
int key;
int val;
Node prev;
Node next;
public Node() {this(0, 0);}
public Node(int key, int val) {this.key = key; this.val = val;}
}
private int capacity;
private Node head;
private Node tail;
private Map<Integer, Node> map;
private int size;
public LRUCache(int capacity) {
this.capacity = capacity;
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
map = new HashMap<>();
this.size = 0;
}
private void addNodeToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveNodeToHead(Node node) {
removeNode(node);
addNodeToHead(node);
}
private Node popTail() {
Node node = tail.prev;
removeNode(tail.prev);
return node;
}
public int get(int key) {
if(!map.containsKey(key)) {
return -1;
}
Node node = map.get(key);
moveNodeToHead(node);
return node.val;
}
public void put(int key, int value) {
if(map.containsKey(key)) {
Node node = map.get(key);
node.val = value;
moveNodeToHead(node);
return ;
} else {
size++;
Node node = new Node(key, value);
addNodeToHead(node);
map.put(key, node);
if(size > this.capacity) {
Node n = popTail();
map.remove(n.key);
size--;
}
}
}
}
代码比较简洁易懂!
最后总结一下:使用方案1,占用的内存空间更小,因为双向链表和hash表共用了同一个key,value。