目录
一、什么是LRU Cache?
二、LRU Cache的实现
1.JDK中类似LRUCahe的数据结构LinkedHashMap
2.自己实现双向链表
三、LRU Cache的OJ
一、什么是LRU Cache?
LRU Cache(Least Recently Used的缩写,即最近最少使用,它是一种Cache的替换算法。看Cache替换算法这篇文章)是一种常见的缓存淘汰算法。用于在有限的缓存空间中管理数据对象。LRU Cache 的核心思想是基于时间局部性原理,即最近被访问的数据在未来可能会被再次访问。
注意:LRU Cache 应该更准确地归类为一种缓存淘汰算法,而非传统意义上的数据结构。尽管 LRU Cache 在实现时通常会利用数据结构(如双向链表和哈希表),但它本身更像是一种策略,用于管理缓存中的数据对象。
二、LRU Cache的实现
实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。
使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,双向链表用于记录数据对象的访问顺序,每当一个数据对象被访问时,就将其移动到链表的头部。这样,链表头部的数据对象就是最近被访问的数据,而链表尾部的数据对象则是最久未被访问的数据。同时,使用哈希表能够以 O(1) 的时间复杂度进行数据对象的查找。
当缓存空间达到上限时,需要淘汰最久未被访问的数据对象。这时只需从链表尾部删除相应的数据对象,并在哈希表中删除对应的索引即可。
1.JDK中类似LRUCahe的数据结构LinkedHashMap
LinkedHashMap中有一个这样的构造方法:
重点的accessOrder:
accessOrder 是一个 boolean 类型的参数,用于指定是否按照访问顺序来排序条目。当accessOrder 被设置为 true 时,表示按照访问顺序排序条目;当 accessOrder 被设置为 false 或未指定时(默认情况下),则按照插入顺序排序条目。
示例1:当accessOrder的值为false的时候
输出结果:
import java.util.LinkedHashMap;
import java.util.Map;public class LRUCache extends LinkedHashMap<Integer, Integer> {private final int capacity;public LRUCache(int capacity) {//accessOrder设置为false时,会按照插入顺序进行排序,当accessOrder为true时,会按照访问顺序super(capacity, 0.75f, true);this.capacity = capacity;}@Overridepublic Integer get(Object key) {//如果get不到,返回默认值-1return super.getOrDefault(key, -1);}@Overridepublic Integer put(Integer key, Integer value) {return super.put(key, value);}@Overrideprotected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {//如果集合内元素个数超过capacity,会将最不常用的元素出队,并将新元素插在尾部return size() > capacity;}
}
测试:
当然在面试时这个做法肯定不会符合面试官的要求,更好的做法是自己实现一个双向链表。
2.自己实现双向链表
前面说到:
- 双向链表用于记录数据对象的访问顺序,每当一个数据对象被访问时,就将其移动到链表的头部。这样,链表头部的数据对象就是最近被访问的数据,而链表尾部的数据对象则是最久未被访问的数据。同时,使用哈希表能够以 O(1) 的时间复杂度进行数据对象的查找。
- 当缓存空间达到上限时,需要淘汰最久未被访问的数据对象。这时只需从链表尾部删除相应的数据对象,并在哈希表中删除对应的索引即可。
为方便测试,我重写了toString方法,测试代码也包含在里面,根据需求删除即可。
import java.util.HashMap;
import java.util.Map;//双向链表 + 哈希表
public class MyLRUCache {//双向链表的节点类static class DLinkedNode {//键值对int key;int val;DLinkedNode next;DLinkedNode prev;public DLinkedNode() {}public DLinkedNode(int key, int val) {this.key = key;this.val = val;}}private Map<Integer, DLinkedNode> cache; //缓存private final int capacity;private int size;//虚拟的头尾节点(哨兵)private DLinkedNode head;private DLinkedNode tail;public MyLRUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>();//连接虚拟头尾节点this.head = new DLinkedNode();this.tail = new DLinkedNode();head.next = tail;tail.prev = head;}//插入数据public void put(int key, int value) {//两种情况:元素是否存在if (!cache.containsKey(key)) {//1.元素不存在//1.1 新元素加到cache集合中DLinkedNode newNode = new DLinkedNode(key, value);cache.put(key, newNode);//1.2 插到尾部addLast(newNode);size++;//1.3 判断当前的元素个数是否超过容量if (size > capacity) {//1.4 删除第一个节点,即最近最久未使用DLinkedNode del = removeFirst();//同时需要把该元素从cache中删除cache.remove(del.key);size--;}} else {//2.元素存在,修改链表的顺序,将该节点放到尾部(最近一次使用的)DLinkedNode node = cache.get(key);//2.1 以新的值将该节点移动到尾部node.val = value;moveLast(node);}}//将节点移动到尾部private void moveLast(DLinkedNode node) {//1.删除当前节点removeNode(node);//2.尾插到链表addLast(node);}//删除节点private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;}//删除第一个节点private DLinkedNode removeFirst() {DLinkedNode del = head.next;head.next = del.next;head.next.prev = head;return del;}//尾插到链表private void addLast(DLinkedNode node) {//连接最后一个节点和新的尾节点tail.prev.next = node;node.prev = tail.prev;//连接新尾节点和傀儡尾节点tail.prev = node;node.next = tail;}//获取数据public int get(int key) {//1.cache集合中拿到这个节点DLinkedNode node = cache.get(key);//2.判断是否有这个节点if (node != null) {//2.1 有该节点 将该节点移动到尾部(最近一次使用的)moveLast(node);return node.val;}//2.2 没有该节点,返回-1return -1;}//删除数据public boolean remove(int key) {//1.看缓存中存不存在DLinkedNode node = cache.get(key);//2.不存在,直接返回if (node == null) {return false;}//3.存在//3.1 删除链表的节点removeNode(node);//3.2 删除cache集合的数据cache.remove(key);size--;return true;}@Overridepublic String toString() {StringBuilder sbu = new StringBuilder();DLinkedNode cur = head.next;sbu.append("{");while (cur != tail) {if (cur.next != tail) {sbu.append(cur.key).append("=").append(cur.val).append(", ");} else {sbu.append(cur.key).append("=").append(cur.val);}cur = cur.next;}sbu.append("}");return sbu.toString();}public static void main(String[] args) {MyLRUCache lruCache = new MyLRUCache(3);lruCache.put(1, 10);lruCache.put(2, 11);lruCache.put(3, 12);System.out.println(lruCache);System.out.println("使用后:");lruCache.get(2);System.out.println(lruCache);lruCache.get(1);System.out.println(lruCache);lruCache.put(4, 13);System.out.println("最不常用的被删除,新元素插到尾部:");System.out.println(lruCache);}
}
三、LRU Cache的OJ
LeetCode 热题100 链表专题的最后一题
146. LRU 缓存 - 力扣(LeetCode)https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/?envType=study-plan-v2&envId=top-100-liked前面两种实现中,MyLRUCache的get方法在获取不到数据时返回的是-1的原因就是根据这道题的要求做的,并且多写了一个remove方法。提交时将类名和构造方法改成原题默认的LRUCache,且不要main方法即可。