当前位置: 首页 > news >正文

缓存淘汰算法LRU与LFU实现原理与JAVA实现

深入理解缓存淘汰算法:LRU与LFU实现原理与代码详解

一、缓存淘汰算法概述

缓存淘汰算法是计算机科学中用于管理有限缓存空间的重要策略。当缓存空间不足时,这些算法决定哪些数据应该被保留,哪些应该被淘汰。最常见的两种淘汰算法是:

  • LRU (Least Recently Used):最近最少使用算法
  • LFU (Least Frequently Used):最不经常使用算法

二、LRU缓存实现详解

1. 核心思想

LRU基于"最近最少使用"原则淘汰数据,通过维护访问顺序来实现。

2. 完整实现代码

import java.util.LinkedList;
import java.util.HashMap;
import java.util.Map;

class LRUCache {
    // 使用双向链表维护访问顺序(最近访问的键位于头部)
    private final LinkedList<Integer> accessOrder;
    // 使用HashMap存储键值对,实现O(1)时间复杂度的访问
    private final Map<Integer, Integer> cacheMap;
    // 缓存容量(final修饰,确保不可变)
    private final int capacity;

    /**
     * 构造函数,初始化缓存容量和数据结构
     * @param capacity 缓存最大容量
     */
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.accessOrder = new LinkedList<>();
        this.cacheMap = new HashMap<>(capacity); // 初始化指定容量以提高效率
    }

    /**
     * 获取键对应的值,并更新访问顺序
     * @param key 目标键
     * @return 对应的值,若不存在则返回-1
     */
    public int get(int key) {
        if (!cacheMap.containsKey(key)) {
            return -1; // 键不存在时返回-1
        }
        // 更新该键的访问顺序(移到链表头部)
        updateAccessOrder(key);
        return cacheMap.get(key);
    }

    /**
     * 插入或更新键值对,维护缓存容量
     * @param key 目标键
     * @param value 对应的值
     */
    public void put(int key, int value) {
        if (capacity <= 0) {
            return; // 容量为0时不执行任何操作
        }

        if (cacheMap.containsKey(key)) {
            // 键已存在:更新值并调整访问顺序
            updateAccessOrder(key);
        } else {
            // 键不存在:检查是否需要淘汰旧数据
            if (accessOrder.size() >= capacity) {
                evictLeastRecentlyUsed(); // 淘汰最久未使用的键
            }
            // 将新键插入到访问顺序的头部
            accessOrder.addFirst(key);
        }
        // 更新或插入键值对(HashMap会自动处理更新逻辑)
        cacheMap.put(key, value);
    }

    /**
     * 将指定键移动到访问顺序的头部(最近访问)
     * @param key 目标键
     */
    private void updateAccessOrder(int key) {
        accessOrder.remove((Integer) key); // 从链表中移除旧位置(需拆箱为Integer)
        accessOrder.addFirst(key); // 插入到头部
    }

    /**
     * 淘汰最久未使用的键(链表尾部元素)
     */
    private void evictLeastRecentlyUsed() {
        int removedKey = accessOrder.removeLast(); // 移除链表尾部元素(最久未使用)
        cacheMap.remove(removedKey); // 同步从HashMap中删除
    }
}

3. 关键点解析

  1. LinkedList维护访问顺序,头部是最新访问的数据
  2. HashMap提供O(1)时间的键值访问
  3. 访问数据时通过updateAccess方法更新位置
  4. 容量满时从链表尾部淘汰最久未使用的数据

4. 复杂度分析

  • 时间复杂度:
    • get操作:O(1)
    • put操作:O(1)
  • 空间复杂度:O(capacity)

三、LFU缓存实现详解

1. 核心思想

LFU基于"使用频率最低"原则淘汰数据,同时考虑访问频率和访问时间。

2. 完整实现代码

import java.util.LinkedHashSet;
import java.util.HashMap;
import java.util.Map;

class LFUCache {
    // 缓存容量(final修饰,确保不可变)
    private final int capacity;
    // 当前最小访问频率(用于快速定位需要淘汰的键)
    private int minFrequency;
    // 键到值的映射(存储实际数据)
    private final Map<Integer, Integer> keyToValue;
    // 键到访问频率的映射
    private final Map<Integer, Integer> keyToFrequency;
    // 频率到键集合的映射(使用LinkedHashSet维护相同频率下的访问顺序)
    private final Map<Integer, LinkedHashSet<Integer>> frequencyToKeys;

    /**
     * 构造函数,初始化缓存容量和数据结构
     * @param capacity 缓存最大容量
     */
    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.minFrequency = 0; // 初始最小频率为0(无数据时)
        this.keyToValue = new HashMap<>();
        this.keyToFrequency = new HashMap<>();
        this.frequencyToKeys = new HashMap<>();
    }

    /**
     * 获取键对应的值,并更新访问频率
     * @param key 目标键
     * @return 对应的值,若不存在则返回-1
     */
    public int get(int key) {
        if (!keyToValue.containsKey(key)) {
            return -1; // 键不存在时返回-1
        }
        // 增加该键的访问频率
        increaseFrequency(key);
        return keyToValue.get(key);
    }

    /**
     * 插入或更新键值对,维护缓存容量和频率信息
     * @param key 目标键
     * @param value 对应的值
     */
    public void put(int key, int value) {
        if (capacity <= 0) {
            return; // 容量为0时不执行任何操作
        }

        if (keyToValue.containsKey(key)) {
            // 键已存在:更新值并增加频率
            keyToValue.put(key, value);
            increaseFrequency(key);
            return;
        }

        // 键不存在:检查是否需要淘汰旧数据
        if (keyToValue.size() >= capacity) {
            evictLeastFrequent(); // 淘汰访问频率最低的键
        }

        // 插入新键值对(初始频率为1)
        keyToValue.put(key, value);
        keyToFrequency.put(key, 1);
        // 初始化频率为1的键集合(若不存在则创建)
        frequencyToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
        // 新插入数据后,最小频率必定为1(初始插入或淘汰后重置)
        this.minFrequency = 1;
    }

    /**
     * 增加指定键的访问频率,并更新相关数据结构
     * @param key 目标键
     */
    private void increaseFrequency(int key) {
        int currentFrequency = keyToFrequency.get(key);
        // 频率加1
        keyToFrequency.put(key, currentFrequency + 1);

        // 从原频率集合中移除该键
        LinkedHashSet<Integer> oldFrequencySet = frequencyToKeys.get(currentFrequency);
        oldFrequencySet.remove(key);
        // 如果原频率集合为空,移除该频率条目
        if (oldFrequencySet.isEmpty()) {
            frequencyToKeys.remove(currentFrequency);
            // 如果移除的是当前最小频率,需要更新minFrequency
            if (currentFrequency == minFrequency) {
                minFrequency++;
            }
        }

        // 将键添加到新频率的集合中(自动创建集合若不存在)
        frequencyToKeys.computeIfAbsent(currentFrequency + 1, k -> new LinkedHashSet<>()).add(key);
    }

    /**
     * 淘汰访问频率最低的键(优先淘汰频率最低且最久未使用的键)
     */
    private void evictLeastFrequent() {
        // 获取最小频率对应的键集合
        LinkedHashSet<Integer> keysToEvict = frequencyToKeys.get(minFrequency);
        // 获取该集合中最早插入的键(LinkedHashSet按插入顺序存储)
        int keyToRemove = keysToEvict.iterator().next();
        // 从集合中移除该键
        keysToEvict.remove(keyToRemove);
        // 如果集合为空,移除该频率条目(无需更新minFrequency,put操作会重置)
        if (keysToEvict.isEmpty()) {
            frequencyToKeys.remove(minFrequency);
        }
        // 从核心数据结构中移除该键
        keyToValue.remove(keyToRemove);
        keyToFrequency.remove(keyToRemove);
    }
}

3. 关键点解析

  1. 使用三个核心数据结构:
    • keyToValue:存储键值对
    • keyToFrequency:记录访问频率
    • frequencyToKeys:维护相同频率下的访问顺序
  2. LinkedHashSet保证同频率下的时序性
  3. minFrequency变量高效定位淘汰目标
  4. 新插入数据的初始频率总是1

4. 复杂度分析

  • 时间复杂度:
    • get操作:O(1)平均
    • put操作:O(1)平均
  • 空间复杂度:O(capacity)

四、两种算法对比

特性LRULFU
淘汰原则最近最少使用使用频率最低
数据结构哈希表+双向链表三重映射结构
时间复杂度O(1)访问和插入O(1)平均时间复杂度
优势场景突发流量、时间局部性强的访问模式稳定流量、长期热点数据
实现复杂度简单较复杂

五、生产环境建议

  1. 选择依据

    • LRU适合时间局部性强的场景
    • LFU适合需要识别长期热点的场景
  2. 优化建议

    • 使用Caffeine等成熟缓存库
    • 高并发场景采用分片缓存策略
  3. 扩展思考

    • 可结合TTL过期策略
    • 考虑LRU-K等混合算法

相关文章:

  • 98页PPT波士顿咨询:制造业数字化转型战略规划方案及变革指南
  • JSP运行环境安装及常用HTML标记使用
  • esp32cam远程图传:AI Thinker ESP32-CAM -》 服务器公网 | 服务器 -》 电脑显示
  • LangChain4j(5):LangChain4j实现RAG之RAG简介
  • leetcode_19. 删除链表的倒数第 N 个结点_java
  • 【补题】P10424 [蓝桥杯 2024 省 B] 好数(数位dp)
  • LabVIEW驱动开发的解决思路
  • 《微服务与事件驱动架构》读书分享
  • 宝塔面板数据库管理页面打不开,提示405 Not Allowed
  • 强化学习Double DQN模型详解
  • C基础笔记_指针专题
  • zk基础—5.Curator的使用与剖析一
  • 【FreeRTOS】二值信号量 是 消息队列 吗
  • FPGA_BD Block Design学习(一)
  • VBA高级应用30例应用4:打开工作薄时进行身份验证
  • 记录vscode连接不上wsl子系统下ubuntu18.04问题解决方法
  • LeetCode 3375 题解
  • LibreOffice 自动化操作目录
  • 常见算法模板总结
  • 高压安全新挑战:新能源汽车三电系统绝缘材料的漏电流与击穿特性研究
  • 怎样做网站漂浮/丽水网站seo
  • 昆明网站做/sem账户托管外包
  • 视频网站做短视频/做一个企业网站需要多少钱
  • 赣州省住房和城乡建设厅网站/东莞百度快速优化排名
  • 南昌做网站比较好的公司/宁波seo关键词
  • 手机网站做桌面快捷方式/seo整站优化哪家好