LRU算法
很多Cache都支持LRU(Least Recently Used)算法,LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
LRU Cache一般支持两个操作:
- get(key),如果key在cache中,则返回对应的value值,否则返回-1;
- set(key,value),如果key在cache中,则重置value的值;如果key不在cache中,则将该(key,value)插入cache中(注意,如果cache已满,则必须把最近最久未使用的元素从cache中删除);
而用什么数据结构来实现LRU算法呢?最常见的实现是使用一个链表保存缓存数据,如下图:
算法如下:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
这种链表结构实现简单,但效率不高,每次请求时都需要遍历链表,需要O(N)的复杂度;下面考虑一种更复杂的实现方式。
使用Hash表+双向链表的实现:hash表保证get操作在O(1)时间复杂度完成,双向链表保证增加/删除操作在O(1)时间完成;
实现原理:
get方法:
- 如果hash表不存在,直接返回;
- 若存在,则将这个key从双链表移动到头部;
set方法:
- 如果hash表不存在,写入hash表,并写入双链表头部;
- 若存在,则将这个key从双链表移动到头部;
一个Java实现版本
class Node{
int key;
int value;
Node pre;
Node next; public Node(int key, int value){
this.key = key;
this.value = value;
}
} public class LRUCache {
int capacity;
HashMap<Integer, Node> map = new HashMap<Integer, Node>();
Node head=null;
Node end=null; public LRUCache(int capacity) {
this.capacity = capacity;
} public int get(int key) {
if(map.containsKey(key)){
Node n = map.get(key);
remove(n);
setHead(n);
return n.value;
} return -1;
} public void remove(Node n){
if(n.pre!=null){
n.pre.next = n.next;
}else{
head = n.next;
} if(n.next!=null){
n.next.pre = n.pre;
}else{
end = n.pre;
} } public void setHead(Node n){
n.next = head;
n.pre = null; if(head!=null)
head.pre = n; head = n; if(end ==null)
end = head;
} public void set(int key, int value) {
if(map.containsKey(key)){
Node old = map.get(key);
old.value = value;
remove(old);
setHead(old);
}else{
Node created = new Node(key, value);
if(map.size()>=capacity){
map.remove(end.key);
remove(end);
setHead(created); }else{
setHead(created);
} map.put(key, created);
}
}
}