LRU缓存是什么?&力扣相关题目

前言

在系统中,特别是后端,我们经常会遇到以下问题:

  • 数据库 QPS 成为系统瓶颈

  • 热点数据被频繁访问

  • 网络 / 磁盘 IO 成本高

缓存(Cache) 的核心目标只有一个:

用空间换时间,减少昂贵资源的访问次数

而在缓存系统中,一个绕不开的问题是:

缓存满了,该淘汰谁?

这就引出了缓存淘汰算法,其中最经典、最常用的就是 ------ LRU(Least Recently Used)最近最少使用算法。

一、LRU是什么?

LRU(最近最少使用) 的核心思想非常直观:

如果一条数据最近被访问过,那么它将来被再次访问的概率也更高

因此:

  • 优先保留「最近访问过」的数据

  • 淘汰「很久没被访问」的数据

举个栗子:

使用情况为:A → B → C → A → D的缓存,最先被淘汰的是哪个?

答案是:B,它最久未被访问,所以会最先被淘汰

二、如何实现?

自己写一个(leetcode146LRU缓存)

java 复制代码
class LRUCache {

    /**
     * 双向链表节点
     * 用于维护访问顺序
     */
    class DLinkedList {
        // 前驱节点
        DLinkedList prev;
        // 后继节点
        DLinkedList next;
        // 缓存的 value
        int value;
        // 缓存的 key(用于从 HashMap 中删除)
        int key;

        // 无参构造(用于创建虚拟头尾节点)
        public DLinkedList() {}

        // 有参构造(用于创建真实缓存节点)
        public DLinkedList(int value, int key) {
            this.key = key;
            this.value = value;
        }
    }

    /**
     * 虚拟头节点(不存数据)
     * head.next 指向最近访问的节点
     */
    private DLinkedList head;

    /**
     * 虚拟尾节点(不存数据)
     * tail.prev 指向最久未访问的节点
     */
    private DLinkedList tail;

    // 缓存最大容量
    private int capacity;

    // 当前缓存大小
    int size;

    /**
     * HashMap:key -> 双向链表节点
     * 作用:O(1) 时间复杂度定位节点
     */
    private HashMap<Integer, DLinkedList> cache = new HashMap<>();

    /**
     * 初始化 LRUCache
     */
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;

        // 初始化虚拟头尾节点
        head = new DLinkedList();
        tail = new DLinkedList();

        // 构建初始双向链表结构:head <-> tail
        head.next = tail;
        tail.prev = head;
    }

    /**
     * 获取缓存
     * @param key
     * @return value 或 -1
     */
    public int get(int key) {
        // 从 HashMap 中查找节点
        DLinkedList node = cache.get(key);

        // 不存在,直接返回 -1
        if (node == null) {
            return -1;
        }

        // 存在则说明被访问,需要移动到链表头部
        moveToHead(node);

        // 返回节点值
        return node.value;
    }

    /**
     * 写入缓存
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        // 尝试从缓存中获取节点
        DLinkedList node = cache.get(key);

        // 如果 key 不存在
        if (node == null) {
            // 创建新节点
            DLinkedList newNode = new DLinkedList(value, key);

            // 缓存数量 +1
            ++size;

            // 放入 HashMap
            cache.put(key, newNode);

            // 新节点是最近访问的,插入到链表头部
            addToHead(newNode);

            // 如果超出容量,淘汰最久未使用节点
            if (size > capacity) {
                // 移除链表尾部节点
                DLinkedList removed = removeTail();

                // 从 HashMap 中删除对应 key
                cache.remove(removed.key);

                // 缓存数量 -1
                size--;
            }

        } else {
            // key 已存在,更新 value
            node.value = value;

            // 更新后同样视为最近访问
            moveToHead(node);
        }
    }

    /**
     * 将节点添加到链表头部
     * head <-> node <-> 原 head.next
     */
    private void addToHead(DLinkedList node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    /**
     * 从链表中移除指定节点
     */
    private void removeNode(DLinkedList node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    /**
     * 将指定节点移动到链表头部
     * 本质:先删除,再插入头部
     */
    private void moveToHead(DLinkedList node) {
        removeNode(node);
        addToHead(node);
    }

    /**
     * 移除并返回链表尾部节点
     * 即最久未使用的节点
     */
    private DLinkedList removeTail() {
        // tail.prev 是真实尾节点
        DLinkedList prev = tail.prev;
        removeNode(prev);
        return prev;
    }
}

Java自带-LinkedHashMap

LinkedHashMap 的本质

LinkedHashMap =
HashMap + 双向链表

它在 HashMap 的基础上,额外维护了一条双向链表,用来记录元素顺序

那么就可以很轻松用它实现LRU

java 复制代码
int MAX_CAPACITY = 100;

Map<Integer, Integer> lruCache =
    new LinkedHashMap<Integer, Integer>(16, 0.75f, true) {

        @Override
        protected boolean removeEldestEntry(
                Map.Entry<Integer, Integer> eldest) {
            return size() > MAX_CAPACITY;
        }
    };

总结

LRU 是一种基于"最近访问时间"的缓存淘汰算法,通常使用 HashMap + 双向链表实现,保证 get / put 操作均为 O(1),在实际工程中被广泛应用。

相关推荐
Mikko74 分钟前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
知了学历孙老师5 分钟前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Java爱好狂.24 分钟前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
秋饼1 小时前
LLM API密钥动态轮换实战:Spring Vault + Spring AI + 多Provider路由
java·ai·技术分享·后端开发
ShineWinsu2 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说3 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
紧跟先前的步伐4 小时前
Go AI Agent 实战 03|Token、上下文窗口与模型调用成本
java·人工智能·golang
const_qiu5 小时前
第四章 00:开篇 —— 从「会聊天」到「记得住、会干活、能协作」
java·开发语言·人工智能·python
Wang's Blog5 小时前
Java 项目实战: 交友项目 - 个人资料管理的更新接口实现与坑
java·开发语言·交友
Promising_GEO5 小时前
GEE实战02期:从卫星波段开始:利用 Sentinel-2 自己计算 NDVI
java·sentinel·遥感·空间分析