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),在实际工程中被广泛应用。

相关推荐
麦兜*2 小时前
SpringBoot集成Redis缓存,提升接口性能的五大实战策略
spring boot·redis·缓存
茶本无香2 小时前
@Scheduled(cron = “0 */5 * * * ?“) 详解
java·定时任务·scheduled
yaonoran2 小时前
【无标题】
java·开发语言·变量
康小庄2 小时前
浅谈Java中的volatile关键字
java·开发语言·jvm·spring boot·spring·jetty
vx_bisheyuange2 小时前
基于SpringBoot的海鲜市场系统
java·spring boot·后端·毕业设计
康康的AI博客2 小时前
工业数据中台:PLC、SCADA、MES的实时协同架构
java·服务器·网络
それども3 小时前
为什么要加@ResponseBody
java·开发语言·spring boot
一只专注api接口开发的技术猿3 小时前
微服务架构下集成淘宝商品 API 的实践与思考
java·大数据·开发语言·数据库·微服务·架构
2501_944424123 小时前
Flutter for OpenHarmony游戏集合App实战之记忆翻牌配对消除
android·java·开发语言·javascript·windows·flutter·游戏