目录
前言
LRU(Least Recently Used),即最近最少使用之意,是一种缓存淘汰策略,当需要淘汰一个数据时(如缓存空间已满),其将一个未使用时间最长的数据淘汰(删除)
本文只介绍最常用的LRU手写实现方法,即HashMap+链表的实现方法
适用于 Leetcode 146
源码
一、设计思想
利用链表存储被存入容器的key,每当有一个数据被放入容器,则将此数据的键头插到链表中;
当有一个数据被读取/更新时,视为该数据被使用,将该数据的键在链表中的位置置于链表头;
当容器已满,又有新数据被放入,则淘汰最久未被使用的数据,即链表尾的数据。
二、实现后的效果
LRU<Integer,String> lru = new LRU<>(5);
lru.put(1,"a");
lru.put(2,"b");
lru.put(3,"c");
lru.put(4,"d");
lru.put(5,"e");
System.out.println(lru.get(1)); // a , key: 1 被使用
lru.put(6,"f"); // 此时, key: 2 是最久未被使用的, < 2,"b"> 被淘汰
System.out.println(lru.get(3)); // c
lru.put(7,"g"); // 此时, key: 4 是最久未被使用的, <4,"d"> 被淘汰
三、实现步骤
1. 类定义
//手写一个LRU容器
public class LRU<K,V> {
// 容器大小
private int size;
// 链表存储key
private LinkedList<K> linkedList;
// HashMap存储键值对
private HashMap<K,V> hashMap;
public LRU(int size){
this.size=size;
this.linkedList=new LinkedList<>();
this.hashMap=new HashMap<>();
}
}
需要用到java.util下的LinkedList和HashMap,有条件的同学可以手写一个双向链表,手写LinkedList可以康康我的从零开始手撕一个数据结构(1)——双向链表
2. 方法定义
1)置入数据操作put方法
public void put(K key,V val){
//检测链表中是否已有该键
if (linkedList.contains(key)){ // 链表中存在该键时
//将该键的位置置于链表头
linkedList.remove(key);
linkedList.addFirst(key);
//更新哈希表
hashMap.put(key,val);
}else { // 链表中不存在该键时
//当容器满,进行淘汰策略
if (linkedList.size()==size){
K knockOutKey = linkedList.getLast();
//在哈希表中删除最久未使用的数据
hashMap.remove(knockOutKey);
//链表尾删
linkedList.removeLast();
}
//数据置入哈希表
hashMap.put(key, val);
//链表头插
linkedList.addFirst(key);
}
}
(有几行重复代码,为了可读性也就不删了。。)
若put方法的key在链表中已存在,执行put方法视为访问了该数据一次
2)获取数据的get方法
public V get(K key){
//检测链表中是否存在该key
if (linkedList.contains(key)){ // 当链表中存在该key时
// 将key置于链头
linkedList.remove(key);
linkedList.addFirst(key);
return hashMap.get(key);
}else { // 链表中不存在该key,返回null
return null;
}
}
get方法应该简单得多,命中缓存时将被命中的数据移到链表头并返回数据,未命中时返回null
3)打印缓存中key的方法
public String keySetString(){
// 当容器为空时,返回null
if (linkedList.size()==0){
return null;
}else { //当容器非空,打印缓存中所有的key
StringBuilder sb = new StringBuilder();
Iterator iterator = linkedList.iterator();
while(iterator.hasNext())
sb.append(iterator.next()).append(",");
return sb.toString();
}
}
这个方法用于方便测试,观察缓存中的数据分布,可有可无
4)返回缓存中的数据个数
public int size(){
return linkedList.size();
}
缓存中的数据个数即链表的长度
四、总结
自此,一个简单的LRU算法就实现了,来测试一下
LRU<Integer,String> lru = new LRU<>(3);
lru.put(1,"a");
lru.put(2,"b");
lru.put(3,"c");
System.out.println(lru.keySetString()); // 3,2,1,
lru.put(4,"d");
//(当要存入的数据多于缓存限定的个数时,淘汰最不常用的数据,即 < 1, "a">
System.out.println(lru.keySetString()); // 4,3,2,
lru.put(2,"z"); // 将 < 2,"b">更新为 < 2, "z">
// key: 2被访问,置于链表头
System.out.println(lru.keySetString()); // 2,4,3,
System.out.println(lru.get(2)); // z
System.out.println(lru.get(3)); // c
lru.put(5,"e");
System.out.println(lru.keySetString()); // 5,3,2,