LRU简介
LRU是“Least Recently Used”的简写,意思是最近最少使用,是一种缓存淘汰策略,在有限的缓存资源中,淘汰掉最近最久未使用的。例如:缓存最大容纳10000条数据,在添加时,只要数据总数小于等于10000可以随意添加,但是当数据量大于1万时,将旧的数据删除,再添加新的数据;添加时,要将新来的数据添加到最前面。说完来添加,再来说查询,从缓存中获取的数据返回之前,需要将数据移动到最前面,因为获取改数据就代表最近使用了,对于缓存来说,最近这个数据很有可能会被再次使用,所以要移动到最前面。
LRU算法分析
因为缓存具有查询快(时间复杂度必须是O(1)),增加快,删除快的特点。同时,LRU策略分析的数据有新旧之分,新的在最前面,旧的往后放。因此选取数据结构时,要遵循有序的特点。因此可以将HashMap与双向链表结合起来实现LRUCache。Hash表查询效率是O(1),但是不是有序的,双向链表(此处不是循环链表)有序并且增加与删除都是O(1),但查询是O(n),所以两者互相取长补短。
实现代码
节点类
public class Node {
public int key,value;
public Node prev;
public Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", value=" + value +
'}';
}
}
双向链表
public class DoubleList {
private LinkedList<Node> linkedList = new LinkedList();
/**添加第一个节点
* @param node
*/
public void addFirst(Node node){
linkedList.addFirst(node);
}
/**
*移除最后一个节点
*/
public Node removeLast(){
return linkedList.removeLast();
}
/**
*移除节点
*/
public void remove(Node node){
linkedList.remove(node);
}
/**
* 链表长度
* @return
*/
public int size(){
return linkedList.size();
}
/**
* 输出双向链表
*/
public void printAll(){
Iterator<Node> iterator = linkedList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
LRUCache类
public class LRUCache {
private HashMap<Integer, Node> map;
private DoubleList cache;
private int cap;
public LRUCache( int cap) {
this.map = new HashMap<Integer, Node>();
this.cache = new DoubleList();
this.cap = cap;
}
/**
* 查询value
**/
public int get(int key){
// 首先判断key是否在map中
if (map.containsKey(key)){
int x = map.get(key).value;
// 在返回之前,将最近访问的键值对添加到LRU中
put(key,x);
return x;
}else {
// 不存在返回-1
return -1;
}
}
// 新增键值对
public void put(int key,int value){
Node node = new Node(key, value);
// 首先判断key是否在map中,如果已存在,map直接新增,覆盖旧的value
if (map.containsKey(key)){
// 链表中删除旧的节点,在头部添加新的节点
cache.remove(map.get(key));
cache.addFirst(node);
map.put(key,node);
}else{
// 在向LRU添加key时,如果缓存已满,要删除最后一个Node,同时map中也要记得删除该key
if (cache.size()==cap){
Node last = cache.removeLast();
map.remove(last.key);
}
cache.addFirst(node);
map.put(key,node);
}
}
// 输出缓存中的数据
public void listAll(){
cache.printAll();
}
}
测试类
class LRUCacheTest {
private LRUCache lruCache = new LRUCache(4);
@Test
public void LRUCacheTest() {
lruCache.put(1, 2);
lruCache.put(2, 3);
lruCache.put(3, 4);
lruCache.put(5, 6);
// 因为cache大小是4,再添加新的数据时,会将最早加入缓存的数据淘汰掉
lruCache.put(6, 67);
// 获取第二次添加的数据,获取后(2,3)会被移动到最前边
lruCache.get(2);
lruCache.listAll();
}
}
所以输出结果:
总结
对于LRU算法,他的优势在于对热点数据的查询,使用LRU可以有效提高查询效率,但是对于批量查询历史数据,有可能导致缓存污染,偶然的查询历史数据,会覆盖掉热点数据,导致本次查询不再走缓存。