LRU (最近最少使用)
文章目录
- LRU (最近最少使用)
- 一、LRU是什么?
- 二、实现
- 1.常规算法
- 2.双栈更替
- 总结
一、LRU是什么?
LRU(Least Recently Used)是一种常见的缓存淘汰策略,核心思想是 “淘汰最长时间未被使用的缓存数据”。它通过追踪数据的访问历史,确保频繁使用的内容保留在缓存中,从而最大化缓存的命中率。
二、实现
1.常规算法
代码如下(示例):
import java.util.HashMap;
import java.util.Map;
// 双向链表节点
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
public class LRUCache {
private Map<Integer, DLinkedNode> cache;
private int capacity;
private int size;
private DLinkedNode head;
private DLinkedNode tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.cache = new HashMap<>();
// 初始化双向链表
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
// 获取缓存值
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 访问后移至链表头部
moveToHead(node);
return node.value;
}
// 添加/更新缓存
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 新增节点
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
// 检查容量
if (size > capacity) {
DLinkedNode removed = removeTail();
cache.remove(removed.key);
size--;
}
} else {
// 更新节点值并移至头部
node.value = value;
moveToHead(node);
}
}
// 将节点添加到链表头部
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
// 从链表中移除节点
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode removed = tail.prev;
removeNode(removed);
return removed;
}
}
2.双栈更替
利用栈的先进后出的特点,假设元素1,2,3依此放入栈中,那么1就在栈底,3就在栈顶,那么元素处于的位置越高,元素就越“新”,这个顺序模拟的是我们取用元素的最新时间,而不是该元素的过期(淘汰)的时间,就像在生活中,我们频繁用到的东西往往会放在明面上,而好久都用不到的东西就会放在很深的角落。
那么对于一个单向栈,我们该如何限制总元素数量以及进行元素淘汰呢,因为需要淘汰的元素都在栈底,所以引入第二个栈B,在淘汰时,将栈A元素依次取出,并依次加入栈B,这时原栈A的栈底元素变成了栈B的栈顶元素,这时依据栈的大小就可以直接进行淘汰了。
代码如下(示例):
import java.util.Stack;
class LRUCache {
private Stack<Integer> stackKey;
private Stack<Integer> stackValue;
private int capacity;
public LRUCache(int capacity) {
this.stackKey = new Stack<>();
this.stackValue = new Stack<>();
this.capacity = capacity;
}
public int get(int key) {
// 先查找key是否存在
int index = stackKey.search(key);
if (index == -1) {
return -1;
} else {
// 为了移动元素到栈顶,需要先弹出上面的元素
Stack<Integer> tempKey = new Stack<>();
Stack<Integer> tempValue = new Stack<>();
// 将目标元素上面的元素暂存到临时栈
for (int i = 0; i < index - 1; i++) {
tempKey.push(stackKey.pop());
tempValue.push(stackValue.pop());
}
// 获取目标元素
int targetKey = stackKey.pop();
int targetValue = stackValue.pop();
// 先将临时栈的元素放回原栈
while (!tempKey.isEmpty()) {
stackKey.push(tempKey.pop());
stackValue.push(tempValue.pop());
}
// 再将目标元素压入栈顶
stackKey.push(targetKey);
stackValue.push(targetValue);
return targetValue;
}
}
public void put(int key, int value) {
// 先检查key是否已存在
int index = stackKey.search(key);
if (index == -1) {
// key不存在,直接添加到栈顶
stackKey.push(key);
stackValue.push(value);
// 检查是否超过容量
if (stackKey.size() > capacity) {
// 移除栈底元素(最久未使用)
// 为了移除栈底元素,需要先弹出所有元素
Stack<Integer> tempKey = new Stack<>();
Stack<Integer> tempValue = new Stack<>();
// 将所有元素移到临时栈
while (!stackKey.isEmpty()) {
tempKey.push(stackKey.pop());
tempValue.push(stackValue.pop());
}
// 移除临时栈的栈底元素(原栈的栈底)
tempKey.pop();
tempValue.pop();
// 将剩余元素放回原栈
while (!tempKey.isEmpty()) {
stackKey.push(tempKey.pop());
stackValue.push(tempValue.pop());
}
}
} else {
// key已存在,需要更新值并移到栈顶
// 先将目标元素上面的元素暂存到临时栈
Stack<Integer> tempKey = new Stack<>();
Stack<Integer> tempValue = new Stack<>();
for (int i = 0; i < index - 1; i++) {
tempKey.push(stackKey.pop());
tempValue.push(stackValue.pop());
}
// 移除目标元素
stackKey.pop();
int oldValue = stackValue.pop();
// 先将临时栈的元素放回原栈
while (!tempKey.isEmpty()) {
stackKey.push(tempKey.pop());
stackValue.push(tempValue.pop());
}
// 将新值添加到栈顶
stackKey.push(key);
stackValue.push(value);
}
}
}
总结
双栈的优点在于不需要使用复杂的数据结构就可以实现,并且在双栈更替的过程可以根据需求进行自定义淘汰标准,在追求低时间复杂度时可以考虑双向链表和哈希表的组合。
双栈的扩展==》多栈,双栈最大的问题在于,在进行淘汰时,每次都需要双栈元素交替,导致需要抛出所有元素,那么采用多栈是否可以优化这一点呢,我们可以依据实际需要来控制单个栈的大小,当元素过多时则新建栈来进行存储,而双栈交替则变成多栈依次交替,而需要淘汰最早的元素时,仅需要拿到栈号,然后简单交替即可,同时,当过期时间引入成为元素的存储依据时,也可以依据建栈时的栈号,来定位元素位置,而更替本身也只需交换栈号即可。