1 编程题:实现一个LRU
背景
自己实现一个简单的LRU的缓存,要求 1 缓存容量是M个, 每次get或者put会提升元素在缓存的优先级。2 超过N分钟的元素没有被访问,要被自动失效掉
思路分析:
功能1: get或者put 更新的时候要把元素移到最优先级的位置。 思路: 最佳的算法是双向链表。 因为双向链表会引用表头和表尾的地址
功能2: 过期自动失效 (比如30分钟 )
2.1 访问的时候判断是否过期,如已过期
2.2 插入的时候启动删除机制。或者定时任务每1s轮询队列尾,看看是否有过期的,有的话取出来,删除,并删除hashmap的原始
方案 1 (不推荐) 使用hashmap和优先队列。优先队列
使用Hashmap存储元素,优先队列存储元素的顺序。 不足:当get某个元素需要修改在优先队列的元素的优先级的时候,性能不好
在 Java 中,PriorityQueue 本身没有直接提供修改元素值的方法。因为优先队列的核心是维护元素的优先级顺序,直接修改元素的值可能会破坏队列的有序性。若要修改优先队列中某个元素的值,一般需要按以下步骤操作:
定位元素:找出优先队列中需要修改的元素。遍历优先队列,时间复杂度O(N)
移除元素:把该元素从优先队列里移除。遍历优先队列,时间复杂度O(N)
修改元素值:对元素的值进行修改。
重新插入元素:将修改后的元素重新插入优先队列,让队列重新调整元素顺序以维护优先级。
思路2 (推荐),使用双向链表和hashmap
为了实现一个具备指定功能的简单 LRU(Least Recently Used,最近最少使用)缓存,我们可以结合双向链表和哈希表来管理缓存项,同时引入时间戳来跟踪每个元素的最后访问时间,以便处理元素的过期问题。以下是 Java 实现代码:
import java.util.HashMap;
import java.util.Map;
// 双向链表节点类,用于存储缓存项信息
class DLinkedNode {
int key;
int value;
long lastAccessTime;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
this.lastAccessTime = System.currentTimeMillis();
}
}
// 自定义 LRU 缓存类
public class LRUCacheWithExpiry {
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private final int capacity;
private final long expiryTimeInMillis;
private DLinkedNode head, tail;
public LRUCacheWithExpiry(int capacity, long expiryMinutes) {
this.capacity = capacity;
this.expiryTimeInMillis = expiryMinutes * 60 * 1000;
this.size = 0;
// 初始化虚拟头节点和尾节点
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;
}
// 检查元素是否过期
if (isExpired(node)) {
removeNode(node);
cache.remove(key);
size--;
return -1;
}
// 若元素未过期,将其移动到链表头部
moveToHead(node);
return node.value;
}
// 插入或更新缓存项
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 若 key 不存在,创建新节点
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 {
// 若 key 存在,更新 value,并将节点移动到链表头部
node.value = value;
node.lastAccessTime = System.currentTimeMillis();
moveToHead(node);
}
// 清理过期元素
cleanExpiredEntries();
}
// 判断元素是否过期
private boolean isExpired(DLinkedNode node) {
return System.currentTimeMillis() - node.lastAccessTime > expiryTimeInMillis;
}
// 清理过期元素
private void cleanExpiredEntries() {
DLinkedNode current = tail.prev;
while (current != head) {
if (isExpired(current)) {
DLinkedNode toRemove = current;
current = current.prev;
removeNode(toRemove);
cache.remove(toRemove.key);
size--;
} else {
break;
}
}
}
// 将节点添加到链表头部
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);
node.lastAccessTime = System.currentTimeMillis();
}
// 移除链表尾部节点
private DLinkedNode removeTail() {
DLinkedNode realTail = tail.prev;
removeNode(realTail);
return realTail;
}
public static void main(String[] args) {
// 创建一个容量为 3,过期时间为 1 分钟的 LRU 缓存
LRUCacheWithExpiry cache = new LRUCacheWithExpiry(3, 1);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3);
System.out.println(cache.get(2));
cache.put(4, 4);
System.out.println(cache.get(1));
System.out.println(cache.get(3));
System.out.println(cache.get(4));
}
}
代码解释
DLinkedNode
类:作为双向链表的节点,存储缓存项的键、值、最后访问时间以及前后指针。LRUCacheWithExpiry
类:cache
:使用HashMap
存储键和对应的节点,便于快速查找。size
:记录当前缓存中的元素数量。capacity
:缓存的最大容量,即M
。expiryTimeInMillis
:元素的过期时间,通过将N
分钟转换为毫秒得到。head
和tail
:虚拟头节点和尾节点,用于简化双向链表的操作。
get
方法:- 先根据键查找节点,若不存在则返回 -1。
- 检查节点是否过期,若过期则移除该节点并返回 -1。
- 若未过期,将节点移动到链表头部并返回其值。
put
方法:- 若键不存在,创建新节点并添加到链表头部和哈希表中。
- 若键已存在,更新值并将节点移动到链表头部。
- 若缓存容量超过
M
,移除链表尾部节点。 - 最后清理过期元素。
- 辅助方法:
isExpired
:判断节点是否过期。cleanExpiredEntries
:从链表尾部开始清理过期元素。addToHead
:将节点添加到链表头部。removeNode
:从链表中移除指定节点。moveToHead
:将指定节点移动到链表头部,并更新其最后访问时间。removeTail
:移除链表尾部节点并返回该节点。
复杂度分析
- 时间复杂度:
get
和put
操作的时间复杂度均为 $O(1)$,清理过期元素的时间复杂度在最坏情况下为 $O(n)$,但在实际应用中,由于过期元素通常较少,整体性能较好。 - 空间复杂度:$O(capacity)$,主要用于存储哈希表和双向链表中的元素。
通过上述代码,我们实现了一个容量为 M
,且元素在 N
分钟未被访问会自动失效的 LRU 缓存。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南