文章目录
- 一、什么是LRU?
- 二、LinkedHashMap 实现LRU缓存
- 三、手写LRU
一、什么是LRU?
LRU是Least Recently Used的缩写,意为最近最少使用。它是一种缓存淘汰策略,用于在缓存满时确定要被替换的数据块。LRU算法认为,最近被访问的数据在将来被访问的概率更高,因此它会优先淘汰最近最少被使用的数据块,以给新的数据块腾出空间。
如图所示:
-
先来3个元素进入该队列
-
此时来了新的元素,因为此时队列中每个元素的使用的次数都相同(都是1),所以会按照LFU的策略淘汰(即淘汰掉最老的那个)
-
此时又来了新的元素,而且是队列是已经存在的,就会将该元素调整为最新的位置。
-
如果此时又来了新的元素,还是”咯咯“,由于”咯咯“已经处于最新的位置,所以大家位置都不变。
-
同理,一直进行上述的循环
二、LinkedHashMap 实现LRU缓存
以力扣的算法题为例子:
力扣146. LRU 缓存
在 jdk 官方的介绍中可以看出,该数据结构天生适合实现 LRU。
代码示例一:
/** * 利用继承 LinkedHashMap 的方式实现LRU缓存 */ class LRUCache extends LinkedHashMap<Integer, Integer> { // 缓存容量 private int capacity; public LRUCache(int capacity) { // 初始化 super(capacity, 0.75f, true); this.capacity = capacity; } public int get(int key) { return super.getOrDefault(key, -1); } public void put(int key, int value) { super.put(key, value); } @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { // 重写比较方法 return super.size() > capacity; } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */
代码示例二:
/** * 利用 LinkedHashMap 特点的方式实现LRU缓存 */ class LRUCache { // 额定容量 private final int CAPACITY; // 使用 LinkedHashMap 的有序排重特点达到要求 private final Map<Integer, Integer> map; public LRUCache(int capacity) { // 初始化 CAPACITY = capacity; map = new LinkedHashMap<>(CAPACITY); } public int get(int key) { Integer value = map.get(key); if (value != null) {// 存在 // 1. 先删除旧的 map.remove(key); // 2. 再添加回去,同时更新了 key 的位置和 value map.put(key, value); // 3. 返回结果 return value; } else {// 不存在 return -1; } } public void put(int key, int value) { if (map.size() == CAPACITY) {// 达到最大容量 if (!map.containsKey(key)) {// 不存在,就删除头 Integer head = map.keySet().iterator().next(); map.remove(head); } else {// 存在,就删除自身 map.remove(key); } } else {// 还有剩余空间,删除自身 map.remove(key); } // 添加新的或 key 更新后的 value map.put(key, value); } }
三、手写LRU
代码示例如下:
/** * https://leetcode.cn/problems/lru-cache/description/ * 手写 LRU 缓存 * Map + 双向链表 */ class LRUCache { // 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 LRUCache(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); } } }