什么是LRU算法?
LRU是一种缓存淘汰策略,对于我们的计算机来说,缓存容量是有限的,那么当缓存满了要怎么办?这时就要用到LRU,对于很久没有用过的数据,我们可以将其判定为无用的数据,当新资源进入缓存或者用到了某个数据的时候,对应的资源可以判定为有用的数据,当缓存满了时我们应当优先淘汰无用的数据,而对于最近使用过的数据应当靠后淘汰,这就是LRU算法。
和手机后台一样,我们会优先杀掉自己很久没用的后台,而常用的则不会杀掉它,甚至还会上锁
实现思路
- 设计一个LRUCache数据结构,有put和get方法,并且时间复杂度为O(1)
- 每次放入或使用数据时,就把这个数据放到第一位,代表近期使用过
- 当缓存容量满时,再放入数据,优先删除最后的数据,把新数据放第一位
- 因为对时间复杂度要求较高,我们首先想到的就是使用HashMap,又因为HashMap是无序的,所以我们还需要一个链表并标记头尾支撑有序性,为了方便操作,该链表可设计为双向链表,总的来说,就是使用哈希表和双向链表来设计LRUCache
- 当放入的数据的key已经存在时,首先在双向链表中删除该结点,然后将这个数据放到第一位,最后对HashMap放入这个数据覆盖掉之前的
- 当LRUCache已满时,先删除链表的尾部数据,然后删除HashMap的对应数据,再把新数据放到链表头部,最后放入HashMap中
图解
代码实现
双向链表Node类
属性包括key、value、前驱结点和后继节点
public class Node {
int key,val;
Node pre,next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
有序双向链表DoubleList类
为了达到有序性,定义头结点head和尾结点tail,因为缓存有容量,所以还有size属性,表示当前链表中元素的数量。我们先不写出最后四个方法,接下来我们会依次讲解这四个方法
public class DoubleList {
private Node head,tail;
private int size;
public DoubleList() {
size = 0;
}
public void addFirst(Node node){}
public void romove(Node node){}
public Node romoveLast(){}
public int size(){}
}
addFirst方法
该方法的作用是将新数据放到链表的头部,利用头插法,时间复杂度为O(1)
public void addFirst(Node node){
//如果此时链表中没有数据,直接将head指向该结点
if(size == 0){
head = node;
}else{ //如果有数据
//使用头插法将该结点放到开头,
node.next = head;
head = node;
//注意这是双向链表,还要管理pre指针
head.next.pre = head;
//当链表中已经有一个数据时,我们把tail指针加入进来,指向head的下一个
if(size == 1){
tail = head.next;
}
}
//链表数据量加1
size++;
}
①如果没有数据,此时head指向node
②如果已经有一个数据,头插法完毕后,加入tail结点
③如果数据量大于1,正常的头插法
romove方法
该方法的作用是移除对应的结点,利用next和pre指针,时间复杂度为O(1)
public void romove(Node node){
//如果该结点是尾结点
if(node.next == null && node.pre != null){
//改变尾结点到上一个结点
tail = node.pre;
node.pre.next = null;
node.pre = null;
//如果该结点是头结点
}else if(node.pre == null && node.next != null){
//改变头结点到下一个结点
head = node.next;
node.next.pre = null;
node.next = null;
//如果该结点是中间结点
}else if(node.pre != null && node.next != null){
//改变上个结点的next
node.pre.next = node.next;
//改变下个结点的pre
node.next.pre = node.pre;
//如果该结点是链表中唯一个结点
}else{
//直接把head搞成null
head = null;
}
//链表数据量减1
size--;
}
romoveLast方法
该方法的作用是移除最后的结点,相当于淘汰掉缓存中最不常用的数据,时间复杂度为O(1)
return该结点是因为在HashMap中也要移除,所以要确定该结点的key
public Node romoveLast(){
Node node;
//如果只有一个数据
if(size == 1){
//记住head结点,然后指向null
node = head;
head = null;
}else{ //如果有多条数据
//记住tail结点并改变tail的位置
node = tail;
tail = tail.pre;
tail.next = null;
}
//链表数据量减1
size--;
return node;
}
size方法
为了判断LRUCache是否已满,没什么好说的,直接return size
public int size() {
return size;
}
LRU缓存LRUCache类
属性包括了,缓存容量capacity,双向链表list以及哈希表map。对于缓存来说,最基本的方法就是put和get方法,所以我们只要实现这两个方法就可以了,后面来依次讲解
public class LRUCache {
int capacity;
DoubleList list;
HashMap<Integer,Node> map;
//初始化
public LRUCache(int capacity) {
this.capacity = capacity;
list = new DoubleList();
map = new HashMap<>();
}
public void put(int key,int value){}
public int get(int key){}
}
put方法
此方法的作用是将相应的数据放入缓存
注意要放到第一位并且要考虑重复和缓存满的情况
public void put(int key,int value){
//new一个Node存放key和value
Node node = new Node(key,value);
//如果HashMap中存在这个key,表示重复了
if (map.containsKey(key)) {
//首先在双向链表中移除这个重复的结点
list.romove(map.get(key));
//然后将新结点放到第一位
list.addFirst(node);
//最后覆盖HashMap中的相同key的数据
map.put(key,node);
//如果HashMap不存在这个key
}else{
//如果缓存已满
if(capacity == list.size()){
//移除最后一个数据,这个数据是很久没有被使用过的
Node last = list.romoveLast();
//HashMap中也要移除
map.remove(last.key);
}
//将新结点放到第一位
list.addFirst(node);
//HashMap中也要存放这个数据
map.put(key,node);
}
}
get方法
此方法的作用是获取某一key值的数据
public int get(int key){
//如果在哈希表中没有这个key,return -1
if (!map.containsKey(key)) {
return -1;
}
//如果存在key,通过HashMap的get()获取到相应的结点并得到val
int res = map.get(key).val;
//因为我们使用到了这个数据,所以要调用put()把这个数据放到第一位
put(key,res);
return res;
}
测试LRUCache
缓存满测试
public class Test {
public static void main(String[] args){
LRUCache cache = new LRUCache(2);
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
System.out.println(cache.get(1));
System.out.println(cache.get(2));
System.out.println(cache.get(3));
}
}
容量为2,key为1的数据是最不常用的数据,应该淘汰,key为3的数据放入
重复key测试
public class Test {
public static void main(String[] args){
LRUCache cache = new LRUCache(2);
cache.put(1,1);
cache.put(2,2);
cache.put(1,4);
cache.put(3,3);
System.out.println(cache.get(1));
System.out.println(cache.get(2));
System.out.println(cache.get(3));
}
}
容量为2,存在key为1的数据重复,覆盖掉,此时key为2的数据是最不常用的数据,应当淘汰,key为3的数据放入
就简单测试一下吧,更复杂的测试大家可以自己来进行,本人能力有限,可能出现考虑不周的地方,如果遇到不对的地方希望可以多多指正,谢谢大家