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

LRU实现

使用 Java 实现 LRU(Least Recently Used,最近最少使用)缓存是一个非常经典的面试题,它考验了候选人对数据结构、面向对象设计以及 Java 集合框架的掌握程度。


LRU 缓存的核心思想

LRU 缓存的核心思想是“如果数据最近被访问过,那么它在将来被访问的概率也更高”。当缓存容量已满,需要插入新数据时,它会淘汰最久未被使用的数据。

为了实现这个逻辑,我们需要满足两个基本操作:

  • 快速查找:当访问一个数据时,需要能够快速判断它是否在缓存中,并且要能快速地更新它的“最近使用”状态。HashMap 的平均 O(1) 时间复杂度的查找非常适合。
  • 快速排序/维护顺序:我们需要一种结构来记录所有数据的“最近使用”顺序,并且当数据被访问或更新时,能快速地将其移动到“最新”的位置,当需要淘汰时,能快速地找到“最旧”的数据。双向链表非常适合这个场景,因为它可以在 O(1) 时间内完成节点的插入和删除。

方法一:使用 LinkedHashMap 实现(标准做法)

LinkedHashMap 是 Java 集合框架中一个特殊的类,它继承自 HashMap,并在其基础上维护了一个双向链表,这个链表记录了元素的插入顺序或访问顺序。

我们可以利用这个“访问顺序”特性来非常轻松地实现 LRU 缓存。

实现步骤
  1. 继承 LinkedHashMap:创建一个类 LRUCache<K, V> 继承 LinkedHashMap<K, V>
  2. 设置访问顺序:在构造函数中,调用父类 LinkedHashMap 的构造函数,并将 accessOrder 参数设置为 true。这会让链表按照元素的访问顺序(从最近访问到最久未访问)进行排序,而不是插入顺序。
  3. 重写 removeEldestEntry 方法LinkedHashMap 本身提供了一个回调方法 removeEldestEntry(Map.Entry eldest)。在每次向 Map 中添加新元素后,此方法都会被调用。当此方法返回 true 时,LinkedHashMap 会自动移除最旧的条目(即链表的头节点)。我们只需在这个方法中判断当前 Map 的大小是否超过了我们设定的容量。
代码实现
import java.util.LinkedHashMap;
import java.util.Map;/*** 使用 LinkedHashMap 实现的 LRU 缓存* @param <K> 键类型* @param <V> 值类型*/
public class LRUCacheWithLinkedHashMap<K, V> extends LinkedHashMap<K, V> {private final int maxCapacity;/*** 构造函数* @param maxCapacity 缓存的最大容量*/public LRUCacheWithLinkedHashMap(int maxCapacity) {// 调用父类构造函数// initialCapacity: 初始容量,设置为 maxCapacity 可以避免扩容// loadFactor: 加载因子,0.75 是默认值// accessOrder: true 表示按访问顺序排序,这是实现 LRU 的关键super(maxCapacity, 0.75f, true);this.maxCapacity = maxCapacity;}/*** 当插入新元素后,此方法会被调用。* 如果返回 true,则会移除最旧的元素。* @param eldest 最旧的元素(即将被移除的元素)* @return true 如果当前大小超过了最大容量,则移除最旧元素*/@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {// 当缓存大小超过最大容量时,返回 true,让 LinkedHashMap 自动移除最旧的条目return size() > maxCapacity;}// 为了演示,我们添加一个 main 方法public static void main(String[] args) {LRUCacheWithLinkedHashMap<Integer, String> cache = new LRUCacheWithLinkedHashMap<>(3);System.out.println("------ 初始插入 1, 2, 3 ------");cache.put(1, "A");cache.put(2, "B");cache.put(3, "C");System.out.println(cache); // 顺序应为 {1=A, 2=B, 3=C} (访问顺序)System.out.println("\n------ 访问键 1,使其变为最新 ------");cache.get(1);System.out.println(cache); // 顺序变为 {2=B, 3=C, 1=A}System.out.println("\n------ 插入键 4,此时应淘汰最旧的键 2 ------");cache.put(4, "D");System.out.println(cache); // 顺序应为 {3=C, 1=A, 4=D}}
}
优点
  • 代码极其简洁:只需要几行核心代码,利用了 Java 库的强大功能。
  • 高效可靠LinkedHashMap 是经过高度优化的,其性能和稳定性都有保障。
  • 易于理解和维护:代码意图非常明确。

方法二:使用 HashMap + 双向链表 实现(从零开始)

这种方式完全由我们自己构建数据结构,能更深刻地理解 LRU 的原理。

实现步骤
  1. 定义双向链表节点:创建一个 Node 类,包含 keyvalueprev(前驱指针)和 next(后继指针)。
  2. 定义数据结构
    • 一个 HashMap<Integer, Node> 用于存储键到节点的映射,实现 O(1) 时间复杂度的查找。
    • 一个双向链表,用于维护节点的访问顺序。链表头部是最新访问的节点,尾部是最久未访问的节点。
    • 两个哨兵节点:head 和 tail,作为链表的头尾哨兵,可以简化边界条件的处理(如插入和删除时无需判断节点是否为 null)。
  3. 实现核心方法
    • get(key):
      1. 通过 HashMap 查找节点。
      2. 如果节点不存在,返回 null 或 -1。
      3. 如果节点存在,将该节点从链表中当前位置移除,并将其添加到链表头部(表示它刚刚被访问过)。最后返回节点的值。
    • put(key, value):
      1. 通过 HashMap 查找节点。
      2. 如果节点已存在(更新操作):
        • 更新节点的 value
        • 将该节点从链表中当前位置移除,并添加到链表头部。
      3. 如果节点不存在(新增操作):
        • 创建一个新节点。
        • 将新节点添加到链表头部。
        • 将新节点存入 HashMap
        • 检查缓存是否已满:如果 HashMap.size() > capacity,则执行淘汰操作。
      4. 淘汰操作 (evict):
        • 获取链表尾部的节点(最久未使用)。
        • 从 HashMap 中移除该节点对应的键。
        • 从链表中移除该节点。
  4. 辅助方法:为了代码清晰,可以封装一些辅助方法,如 addToHead(Node node)removeNode(Node node)removeTail()
代码实现
import java.util.HashMap;
import java.util.Map;/*** 使用 HashMap + 双向链表实现的 LRU 缓存*/
public class LRUCacheWithHashMapAndList {// 双向链表节点定义class Node {int key;int value;Node prev;Node next;public Node(int key, int value) {this.key = key;this.value = value;}}private final int capacity;private final Map<Integer, Node> cacheMap;private final Node head; // 虚拟头节点private final Node tail; // 虚拟尾节点public LRUCacheWithHashMapAndList(int capacity) {this.capacity = capacity;this.cacheMap = new HashMap<>(capacity);// 初始化双向链表,使用虚拟头尾节点简化操作this.head = new Node(-1, -1);this.tail = new Node(-1, -1);head.next = tail;tail.prev = head;}public int get(int key) {Node node = cacheMap.get(key);if (node == null) {return -1; // 未找到}// 节点存在,将其移动到链表头部,表示最近访问moveToHead(node);return node.value;}public void put(int key, int value) {Node node = cacheMap.get(key);if (node != null) {// 节点已存在,更新值并移动到头部node.value = value;moveToHead(node);} else {// 节点不存在,创建新节点Node newNode = new Node(key, value);cacheMap.put(key, newNode);// 添加到链表头部addToHead(newNode);// 检查是否超出容量if (cacheMap.size() > capacity) {// 超出容量,移除尾部节点(最久未使用)Node tailNode = removeTail();cacheMap.remove(tailNode.key);}}}// --- 辅助方法 ---/*** 将节点添加到链表头部*/private void addToHead(Node node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}/*** 从链表中移除指定节点*/private void removeNode(Node node) {node.prev.next = node.next;node.next.prev = node.prev;}/*** 将节点移动到链表头部*/private void moveToHead(Node node) {removeNode(node);addToHead(node);}/*** 移除链表尾部节点,并返回该节点*/private Node removeTail() {Node node = tail.prev;removeNode(node);return node;}// 为了演示,我们添加一个 main 方法public static void main(String[] args) {LRUCacheWithHashMapAndList cache = new LRUCacheWithHashMapAndList(3);System.out.println("------ 初始插入 1, 2, 3 ------");cache.put(1, 10);cache.put(2, 20);cache.put(3, 30);System.out.println("Get 1: " + cache.get(1)); // 输出 10,此时 1 变为最新System.out.println("\n------ 插入键 4,此时应淘汰最旧的键 2 ------");cache.put(4, 40);System.out.println("Get 1 (should be 10): " + cache.get(1)); // 1 还在System.out.println("Get 2 (should be -1): " + cache.get(2)); // 2 已被淘汰System.out.println("Get 3 (should be 30): " + cache.get(3)); // 3 还在System.out.println("Get 4 (should be 40): " + cache.get(4)); // 4 还在}
}
优点
  • 深入理解原理:完全手写一遍能让你对 LRU 的工作机制有透彻的理解。
  • 灵活性高:如果需求有变(例如实现 LFU),可以基于这个结构进行修改。

总结与对比

特性LinkedHashMap 实现HashMap + 双向链表 实现
代码量非常少,核心逻辑只需几行较多,需要定义节点和链表操作
实现难度简单,只需了解 LinkedHashMap 特性中等,需要熟练掌握链表和 HashMap
性能优秀,与手写实现相当优秀,所有操作均为 O(1)
适用场景生产环境、日常开发算法学习、技术面试
核心思想利用 Java 库的现有功能从零构建,展示底层原理
http://www.dtcms.com/a/350002.html

相关文章:

  • 【YOLOv5部署至RK3588】模型训练→转换RKNN→开发板部署
  • 冯·诺依曼架构:现代计算机的基石与瓶颈
  • 创新BIM技术在大型冶金综合管网项目中的应用
  • redis知识点
  • MyBatis-Plus 快速入门 -常用注解
  • response.json()与 json.loads(json_string)有何区别
  • 2025年5月架构设计师案例分析真题回顾,附参考答案、解析及所涉知识点(一)
  • 【Java】 Spring Security 赋能 OAuth 2.0:构建安全高效的现代认证体系
  • spring boot开发:一些基础知识
  • 5分钟了解单元测试
  • 大数据量的ArrayList怎么获取n个元素
  • Ansible 环境配置(基于 RHEL 9)
  • 文件权限详解
  • Allegro-过孔篇(普通VIA,盲埋孔)
  • SOME/IP-SD报文中 Entry Format(条目格式)-理解笔记1
  • 新的 macOS 安装程序声称能够快速窃取数据,并在暗网上销售
  • 第四章:大模型(LLM)】07.Prompt工程-(12)评估prompt的有效性
  • 【LIN】2.LIN总线通信机制深度解析:主从架构、五种帧类型与动态调度策略
  • maven-default-http-blocker (http://0.0.0.0/)
  • Gemini CLI 与 MCP 服务器:释放本地工具的强大潜力
  • Swiper属性全解析:快速掌握滑块视图核心配置!(2.3补充细节,详细文档在uniapp官网)
  • 飞牛影视桌面客户端(fntv-electron)使用教程
  • 无人机航拍数据集|第20期 无人机公路损伤目标检测YOLO数据集3771张yolov11/yolov8/yolov5可训练
  • 一键终结Win更新烦恼!你从未见过如此强大的更新暂停工具!
  • 云手机挂机掉线是由哪些因素造成的?
  • 指纹云手机×Snapchat Spotlight:动态GPS+陀螺仪仿生方案
  • 102. 二叉树的层序遍历
  • 指令集架构ISA是什么?
  • toRefs 和 toRef 的区别和用法
  • 计算机实习经历包装/编写