LRU Java两种简单实现
LRU即Least Recently Used ,最近最少使用的缓存替换规则,此缓存策略关注于程序访问存储介质两个性质之一:时间局部性,最近访问到的内容,接下来大概率会继续访问到。LRU的实现不难,且在java中还有现成的代码可以使用以及学习。下面给出两种LRU的实现:
1.持有或继承LinkedHashMap
LinkedHashMap在HashMap平均O(1)的访问基础上,存储的节点额外增加了前后指向关系:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
注:HashMap中的冲突链表转化为红黑树后,红黑树的node是继承了Entry的,所以也具备该前后指向关系
LinkedHashMap有head、tail成员变量,用于指向前后关系的头和尾。在LinkedHashMap中有个布尔的变量accessOrder,当为true时,此时前后的顺序为访问的时间顺序,为false时,记录的是插入的时间顺序,所以要做LRU缓存的话,需要设置为true。LinkedHashMap在插入和获取时,会调整元素的位置,调整到最新的位置上,即tail变量。这里的head和tail指向关系有点反常识,head指向的是最老的元素,tail指向的是最新的元素。
LinkedHashMap要实现缓存的关键点是重写removeEldestEntry方法,这是移除规则的实现方法,默认实现中是直接返回的false。比较简单的实现是判断当前存放元素的size是否达到的初始设置的容量,如果达到了就移除最久未使用的元素。
完成实现如下:
public class LRUCache_146 extends LinkedHashMap<Integer,Integer> {
private int capacity;
public LRUCache_146(int initialCapacity) {
super(initialCapacity, 0.75f, true);
this.capacity=initialCapacity;
}
public int get(int key) {
return super.getOrDefault(key,-1);
}
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size()>capacity;
}
public void put(int key, int value) {
super.put(key, value);
}
}
这个是继承的实现,持有一个LinkedHashMap的写法类似。
2.自行实现时间的前后关系
在前面的第一种实现里,时间的顺序是由LinkedHashMap维护的,在这个实现里面,自行维护时间顺序,不过在hash映射上依然使用HashMap的能力。
在实现之前,应该大致捋一下里面可能涉及到的变量、方法,有个大致的思路,写起来会清晰很多,笔者抽取的要点如下:
a.三个成员变量:head(指向最年轻的元素)、tail(指向最年老的元素)、capacity(存储的最大容量)
b.当新增元素或者获取已有元素时,元素需插入到head头,即insert2Head方法
c.当获取已有元素时,元素需要断开原前后指向关系,并插入到head头,即move2Head方法
d.当容量满了时,需要移除最老的元素,即removeLastUsed方法
当然存储的元素entry得有前后的指向关系,即pre和next属性。
完成实现如下:
/**
* 最近最少使用的缓存
*/
public class LRUCache {
Map<Integer, Entry> cache;
Entry head, tail;
int capacity;
public LRUCache(int capcity) {
this.capacity = capcity;
cache = new HashMap<>();
}
public int get(int key) {
Entry entry = cache.get(key);
if (entry == null) {
return -1;
}
//调整位置,移动到头部
move2Head(entry);
return entry.val;
}
public void put(int key, int value) {
Entry entry = cache.get(key);
if (entry != null) {
//修改val并调整位置到头部
entry.val = value;
move2Head(entry);
} else {
//超过容量,移除一个最近最久未使用的元素
if (cache.size() == capacity) {
removeLastestUsed();
}
entry = new Entry(key, value);
cache.put(key, entry);
insert2Head(entry);
}
}
/**
* 移除最近最久未使用的元素
*/
private void removeLastUsed() {
if (tail == null) {
return;
}
cache.remove(tail.key);
if (tail == head) {
tail = null;
head = null;
return;
}
Entry pre = tail.pre;
pre.next = null;
tail.pre = null;
tail = pre;
}
/**
* 将元素移动到头部,只对已存在的entry操作
*
* @param entry
*/
private void move2Head(Entry entry) {
Entry pre = entry.pre;
Entry next = entry.next;
if (pre == null) {
//说明是head节点,不用继续操作
return;
}
if (next == null) {
//说明是尾节点,需要更改tail的指向
pre.next = null;
entry.pre = null;
tail = pre;
insert2Head(entry);
return;
}
pre.next = next;
next.pre = pre;
insert2Head(entry);
}
private void insert2Head(Entry entry) {
if (tail == null && head == null) {
head = entry;
tail = entry;
return;
}
Entry head = this.head;
entry.next = head;
entry.pre = null;
head.pre = entry;
this.head = entry;
}
private static class Entry {
int key;
int val;
Entry pre;
Entry next;
public Entry(int key, int val) {
this.key = key;
this.val = val;
}
}
}
其实更狠的是,Hash映射也自己实现,这里先留一个坑位,待后面把Hash映射实现了,再把链接放在这里。