什么是LRU算法
LRU,全称Least Recently Used,即最近最少使用
常被用于缓存淘汰策略,核心思想就是依据缓存的访问时间来降序排序。 最新被访问的缓存排前面,当需要删除缓存时,优先删除排在末尾的缓存
假设现在有一个容量大小为4的LRU列表,对LRU列表的操作主要有两类:
- 写入缓存
- 读取缓存
写入缓存
当写入的缓存不存在时
当写入的缓存不存在时,直接添加到列表头节点位置。当列表长度超过限制时,删除列表的尾节点
依次添加A、B、C、D四个缓存,结果如下图:
添加缓存E,此时列表长度超过4,需要删除尾节点A,结果如下图:
当写入的缓存已存在时
当写入的缓存已存在时,更新对应缓存的值,把对应的节点移到头节点即可。因为缓存已存在,列表的大小不会改变,所以不需要进行删除的操作
写入已存在的缓存B列表前后变化,如下图:
读取缓存
当读取的缓存不存在时,直接返回null(或其他默认值)即可
当读取的缓存存在时,除了返回对应的缓存,还需要把缓存移动到头节点。读取不涉及列表大小的改变,所以也不需要考虑删除操作
读取已存在的缓存B列表前后变化,如下图:
如何实现一个LRU算法
设计思路
LRU列表的主要操作:
- 写入缓存
- 读取缓存
两种操作都会频繁涉及到节点的新增和删除(节点的移动,其实也可以通过先删除,后新增来实现),因此使用链表比数组更加适合
我们经常需要在链表头部添加节点(写入/读取已存在的缓存时,需要把对应节点添加到头节点),在链表尾部删除节点(LRU列表超过最大长度限制时),因此快速定位链表的头尾节点也是一个需要解决的问题
如何快速定位头节点?可以通过添加虚拟头节点来解决,通过head.next快速定位
如何快速定位尾节点?通过添加虚拟尾节点 + 双向链表来解决,通过tail.prev来快速定位
另外我们还需要读取缓存,如果只使用双向链表的话,需要循环遍历获取,时间复杂度为O(N),为了降低读取的时间复杂度,可以使用哈希表来进一步优化
代码
核心思想:双向链表 + 哈希表
LeetCode 146. LRU 缓存
ini
class LRUCache {
// 定义链表节点
class MLinkedNode {
int key;
int value;
MLinkedNode prev;
MLinkedNode next;
public MLinkedNode() {}
public MLinkedNode(int k, int v) {
key = k;
value = v;
}
}
// 链表节点数
private int size;
// 链表最大容量
private int cap;
// 链表节点哈希表
private Map<Integer, MLinkedNode> nodeMap;
// 虚拟头尾节点
private MLinkedNode head;
private MLinkedNode tail;
public LRUCache(int capacity) {
cap = capacity;
nodeMap = new HashMap<Integer, MLinkedNode>();
head = new MLinkedNode();
tail = new MLinkedNode();
// 形成双向链表
head.next = tail;
tail.prev = head;
}
public int get(int key) {
MLinkedNode node = nodeMap.get(key);
if (node == null) {
return -1;
}
// 把对应的key移动到最前面
// 先删除
removeNode(node);
// 后新增
addNodeToHead(node);
return node.value;
}
public void put(int key, int value) {
MLinkedNode node = nodeMap.get(key);
if (node == null) {
// 写入的缓存不存在
node = new MLinkedNode(key, value);
// 添加到链表头节点
addNodeToHead(node);
// 添加到哈希表
nodeMap.put(key, node);
// 链表长度+1
size++;
// 判断链表长度是否超过最大限制
if (size > cap) {
// 删除链表尾节点
MLinkedNode last = tail.prev;
removeNode(last);
// 链表长度-1
size--;
// 从哈希表中删除
nodeMap.remove(last.key);
}
} else {
// 写入的缓存已存在
// 更新缓存的值
node.value = value;
// 把对应的key移动到最前面
// 先删除
removeNode(node);
// 后新增
addNodeToHead(node);
}
}
private void addNodeToHead(MLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(MLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
/**
* 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);
*/