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)); 
    }
}

代码解释

  1. DLinkedNode:作为双向链表的节点,存储缓存项的键、值、最后访问时间以及前后指针。
  2. LRUCacheWithExpiry
    • cache:使用 HashMap 存储键和对应的节点,便于快速查找。
    • size:记录当前缓存中的元素数量。
    • capacity:缓存的最大容量,即 M
    • expiryTimeInMillis:元素的过期时间,通过将 N 分钟转换为毫秒得到。
    • headtail:虚拟头节点和尾节点,用于简化双向链表的操作。
  3. get 方法
    • 先根据键查找节点,若不存在则返回 -1。
    • 检查节点是否过期,若过期则移除该节点并返回 -1。
    • 若未过期,将节点移动到链表头部并返回其值。
  4. put 方法
    • 若键不存在,创建新节点并添加到链表头部和哈希表中。
    • 若键已存在,更新值并将节点移动到链表头部。
    • 若缓存容量超过 M,移除链表尾部节点。
    • 最后清理过期元素。
  5. 辅助方法
    • isExpired:判断节点是否过期。
    • cleanExpiredEntries:从链表尾部开始清理过期元素。
    • addToHead:将节点添加到链表头部。
    • removeNode:从链表中移除指定节点。
    • moveToHead:将指定节点移动到链表头部,并更新其最后访问时间。
    • removeTail:移除链表尾部节点并返回该节点。

复杂度分析

  • 时间复杂度getput 操作的时间复杂度均为 $O(1)$,清理过期元素的时间复杂度在最坏情况下为 $O(n)$,但在实际应用中,由于过期元素通常较少,整体性能较好。
  • 空间复杂度:$O(capacity)$,主要用于存储哈希表和双向链表中的元素。

通过上述代码,我们实现了一个容量为 M,且元素在 N 分钟未被访问会自动失效的 LRU 缓存。

posted @   向着朝阳  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示