文章目录
一、什么是LRU?
LRU是Least Recently Used的缩写,意为最近最少使用。它是一种缓存淘汰策略,用于在缓存满时确定要被替换的数据块。LRU算法认为,最近被访问的数据在将来被访问的概率更高,因此它会优先淘汰最近最少被使用的数据块,以给新的数据块腾出空间。
如图所示:
-
先来3个元素进入该队列
-
此时来了新的元素,因为此时队列中每个元素的使用的次数都相同(都是1),所以会按照LFU的策略淘汰(即淘汰掉最老的那个)
-
此时又来了新的元素,而且是队列是已经存在的,就会将该元素调整为最新的位置。
-
如果此时又来了新的元素,还是"咯咯",由于"咯咯"已经处于最新的位置,所以大家位置都不变。
-
同理,一直进行上述的循环
二、LinkedHashMap 实现LRU缓存
在官方的介绍中可以看出,该数据结构天生适合实现LRU。
代码示例:
java
/**
* 利用 LinkedHashMap 实现LRU缓存(该数据结构天生适合实现 LRU)
* @param <K>
* @param <V>
*/
public class LRUCache1<K, V> extends LinkedHashMap<K, V> {
private final int capacity;// 缓存坑位
public LRUCache1(int capacity) {
// 构造函数中传入了LinkedHashMap的构造函数参数
// true 表示更改存储顺序(LRU),false 表示不更改存储顺序(不是真正的 LRU)
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 指定规则,当缓存满时,移除最老的缓存项(实现LRU的关键)
return super.size() > capacity;
}
public static void main(String[] args) {
// 容量为 3
LRUCache1<String, String> lruCache1 = new LRUCache1<>(3);
// 每一次put,就相当于刷新或插入一次元素
lruCache1.put("1", "1");
lruCache1.put("2", "2");
lruCache1.put("3", "3");
// 此时队列已满
System.out.println("队列已满,如下");
System.out.println(lruCache1.keySet());
// 继续插入新的元素
lruCache1.put("4", "4");
System.out.println("插入新元素 4");
System.out.println(lruCache1.keySet());
// 刷新已经存在的元素
lruCache1.put("3", "3");
System.out.println("已经存在的元素 3");
System.out.println(lruCache1.keySet());
// 刷新已经存在的元素
lruCache1.put("3", "3");
System.out.println("已经存在的元素 3");
System.out.println(lruCache1.keySet());
// 继续插入新的元素
lruCache1.put("5", "5");
System.out.println("插入新元素 5");
System.out.println(lruCache1.keySet());
}
}
输出结果:
三、手写LRU
以力扣的算法题为例子:
代码示例如下:
java
/**
* https://leetcode.cn/problems/lru-cache/description/
* 手写 LRU 缓存
* Map + 双向链表
*/
public class LRUCache2 {
// Map 负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个 Node 节点,作为数据载体。
// 参考 HashMap 中的存储方式,使用内部 Node 维护双向链表
// 1. 构造 Node 节点作为数据载体
public static class Node<K, V> {
public K key;
public V value;
public Node<K, V> prev;
public Node<K, V> next;
public Node() {
this.prev = this.next = null;
}
public Node(K key, V value) {
this.key = key;
this.value = value;
this.prev = this.next = null;
}
}
// 2. 构造一个双向队列,里面存放着 Node 节点
// 队头元素最新,队尾元素最旧
static class DoubleLinkedList<K, V> {
Node<K, V> head;
Node<K, V> tail;
// 2.1 构造方法
public DoubleLinkedList() {
head = new Node<>();
tail = new Node<>();
// 头尾相连
head.next = tail;
tail.prev = head;
}
// 2.2 添加到头(头插)
public void addHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 2.3 删除节点
public void removeNode(Node<K, V> node) {
node.next.prev = node.prev;
node.prev.next = node.next;
node.next = null;
node.prev = null;
}
// 2.4 获取最后一个节点
public Node getLast() {
return tail.prev;
}
}
private final int cacheSize;// 缓存容量
Map<Integer, Node<Integer, Integer>> map;// Map
DoubleLinkedList<Integer, Integer> doubleLinkedList;// 手动实现的双向链表
public LRUCache2(int capacity) {
this.cacheSize = capacity;
map = new HashMap<>();
doubleLinkedList = new DoubleLinkedList<>();
}
public int get(int key) {
if (map.containsKey(key)) {
// 命中,更新双向链表
Node<Integer, Integer> node = map.get(key);
// 先删除双向链表中的节点
doubleLinkedList.removeNode(node);
// 再添加到头部
doubleLinkedList.addHead(node);
return node.value;
} else {
// 未命中,返回 -1
return -1;
}
}
public void put(int key, int value) {
if (map.containsKey(key)) {
// 更新双向链表
Node<Integer, Integer> node = map.get(key);
// 新值替换老值,再放回
node.value = value;
map.put(key, node);
// 先删除双向链表中的节点
doubleLinkedList.removeNode(node);
// 再添加到头部
doubleLinkedList.addHead(node);
} else {
if (map.size() == cacheSize) {
// 超出容量,删除双向链表的最后一个节点
Node lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.removeNode(lastNode);
}
// 新增节点
Node<Integer, Integer> newNode = new Node<>(key, value);
map.put(key, newNode);
doubleLinkedList.addHead(newNode);
}
}
/**
* map 没有顺序,所以需要遍历双向链表,来确定是否是 LRU
* @return
*/
public List<Integer> getAllKeys() {
Node<Integer, Integer> cur = doubleLinkedList.head.next;
List<Integer> ret = new LinkedList<>();
while (cur != doubleLinkedList.tail) {
ret.add(cur.key);
cur = cur.next;
}
return ret;
}
public static void main(String[] args) {
LRUCache2 lruCache2 = new LRUCache2(3);
lruCache2.put(1, 1);
lruCache2.put(2, 2);
lruCache2.put(3, 3);
System.out.println(lruCache2.getAllKeys());
lruCache2.put(4, 4);
System.out.println(lruCache2.getAllKeys());
lruCache2.put(3, 3);
System.out.println(lruCache2.getAllKeys());
}
}
输出结果: