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

深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现

一、LRU (Least Recently Used - 最近最少使用)

LRU 策略的核心思想是:当缓存空间不足时,优先淘汰最近最长时间未被访问的数据。它基于“时间局部性”原理,即最近被访问的数据,在未来被访问的概率也更高。

LeetCode 146. LRU 缓存机制

这道题要求我们设计并实现一个满足 LRU 约束的数据结构。

核心思路:哈希表 + 双向链表

为了同时满足快速查找(get​ 操作)和快速增删(维护访问顺序)的需求,我们采用“哈希表 + 双向链表”的组合:

  1. ​HashMap<Integer, Node>​: 哈希表用于存储键(Key)到链表节点(Node)的映射。这使得我们能够以 O(1) 的平均时间复杂度通过 Key 快速定位到链表中的节点。

  2. 双向链表 (DoubleList​): 双向链表按照节点的访问顺序来组织。

    • 最近使用的节点放在链表尾部。
    • 最久未使用的节点放在链表头部。
    • 当缓存容量满时,淘汰链表头部的节点。
    • 当访问(get​ 或 put​ 更新)一个节点时,将其移动到链表尾部。
    • 添加新节点时,也将其添加到链表尾部。

为什么是这个组合?

  • 哈希表保证了 get​ 操作查找节点的时间复杂度为 O(1)。
  • 双向链表保证了在链表头部删除(淘汰)、在链表尾部添加(新访问/新添加)、以及将任意节点移动到尾部(更新访问)的操作时间复杂度都为 O(1)。(普通链表无法 O(1) 删除任意指定节点)。
  • ​HashMap​ 帮助我们快速找到链表中的节点,然后双向链表快速完成节点的移动或删除。

图示理解:

(此处可想象或引用你提供的图示 image-20240714103929-cde7wui.png​ 和 image-20240714103937-nq23qq9.png​ 来展示数据结构)

Java 实现 (LRUCache​)

import java.util.HashMap;class LRUCache {// Key -> Node(key, val)private HashMap<Integer, Node> map;// 双向链表,存储 Nodeprivate DoubleList cache;// 最大容量private int cap;public LRUCache(int capacity) {this.cap = capacity;map = new HashMap<>();cache = new DoubleList();}/* 将某个 key 提升为最近使用的 */private void makeRecent(Node node) {// 先从链表中删除cache.remove(node);// 再添加到链表尾部cache.addLast(node);}/* 添加最近使用的元素 */private void addRecent(Node node) {// 添加到链表尾部cache.addLast(node);// 同时添加到 map 中map.put(node.key, node);}/* 删除某一个 key 对应的 Node */private void removeNode(Node node) {// 从链表中删除cache.remove(node);// 从 map 中删除map.remove(node.key);}/* 删除最久未使用的元素 (链表头部第一个节点) */private void removeLeastRecent() {// 从链表头部删除节点Node deletedNode = cache.removeFirst();// 如果链表不为空,则从 map 中也删除if (deletedNode != null) {map.remove(deletedNode.key);}}public int get(int key) {if (!map.containsKey(key)) {return -1; // 键不存在}// 键存在,将其变为最近使用Node node = map.get(key);makeRecent(node);return node.val;}public void put(int key, int value) {if (map.containsKey(key)) {// 如果 key 已存在,更新值并将节点移到末尾// 1. 从 map 中删除旧节点 (因为要更新节点,虽然key相同) - 或者直接更新node的val// Node oldNode = map.get(key);// removeNode(oldNode); // 更简单的做法是下面这样// 1. 更新节点值Node node = map.get(key);node.val = value;// 2. 将节点移到末尾makeRecent(node);// 注意:不能简单地 removeNow + addRecent,因为这会创建一个新 Node 对象// removeNow(map.get(key));// addrecent(new Node(key,value)); // 这样做逻辑上是更新,但效率低且可能引入问题return;}// 如果 key 不存在,需要添加新节点// 检查容量是否已满if (cache.size() == cap) {// 删除最久未使用的元素removeLeastRecent();}// 添加新节点到末尾addRecent(new Node(key, value));}// --- 内部类定义 ---class Node {public int key, val;public Node next, pre;public Node(int k, int v) {this.key = k;this.val = v;}}class DoubleList {// 虚拟头尾节点,简化边界处理private Node head, tail;// 链表元素数private int size;public DoubleList() {head = new Node(0, 0);tail = new Node(0, 0);head.next = tail;tail.pre = head;size = 0;}// 在链表尾部添加节点 x(表示最近使用)public void addLast(Node x) {x.pre = tail.pre;x.next = tail;tail.pre.next = x;tail.pre = x;size++;}// 删除链表中的 x 节点(x 一定存在)public void remove(Node x) {x.pre.next = x.next;x.next.pre = x.pre;size--;}// 删除链表中第一个节点(最久未使用),并返回该节点public Node removeFirst() {if (head.next == tail) { // 链表为空return null;}Node first = head.next;remove(first);return first;}// 返回链表长度public int size() {return size;}}
}

关键点总结 (LRU):

  • ​get​ 操作:通过 map​ 找到节点,调用 makeRecent​ 将其移到链表尾部。

  • ​put​ 操作:

    • 若 Key 存在:更新节点 value​,调用 makeRecent​ 将其移到链表尾部。
    • 若 Key 不存在:检查容量,若满则调用 removeLeastRecent​ 淘汰链表头部节点(并从 map​ 移除);然后调用 addRecent​ 将新节点添加到链表尾部和 map​ 中。

二、LFU (Least Frequently Used - 最不经常使用)

LFU 策略的核心思想是:当缓存空间不足时,优先淘汰访问频次最低的数据。如果访问频次最低的数据有多条,则淘汰其中最旧(按访问时间算,即最早进入该最低频次)的数据。

LeetCode 460. LFU 缓存

这道题要求我们设计并实现一个满足 LFU 约束的数据结构,且 get​ 和 put​ 操作的时间复杂度都为 O(1)。

核心思路:哈希表组合 + LinkedHashSet​

LFU 的 O(1) 实现比 LRU 更复杂,需要巧妙地组合多个哈希表:

  1. ​HashMap<Integer, Integer> keyToVal​: 存储 Key 到 Value 的映射,用于 O(1) 获取值。

  2. ​HashMap<Integer, Integer> keyToFreq​: 存储 Key 到其访问频次(Frequency)的映射,用于 O(1) 获取和更新 Key 的频次。

  3. ​HashMap<Integer, LinkedHashSet<Integer>> freqToKeys​: 存储频次(Frequency)到拥有该频次的 Key 集合的映射。

    • 为什么是 LinkedHashSet​?

      • 我们需要一个集合来存储同一频次的所有 Key。
      • 当某个 Key 的频次增加时,需要能 O(1) 地从旧频次的集合中删除该 Key。HashSet​ 提供 O(1) 的平均删除时间。
      • 当频次最低的有多个 Key 时,需要淘汰最旧的 Key。LinkedHashSet​ 在保持 O(1) 增删查的同时,内部维护了元素的插入顺序。因此,当需要淘汰时,迭代 LinkedHashSet​ 的第一个元素即为该频次下最旧的 Key。
      • 普通的 LinkedList​ 无法 O(1) 删除任意指定 Key,而 HashSet​ 不保证顺序。LinkedHashSet​ 是最佳选择。
  4. ​int minFreq​: 一个变量,记录当前缓存中存在的最低访问频次。这使得在需要淘汰时,能 O(1) 定位到最低频次的 Key 集合。

图示理解:

(此处可想象或引用你提供的图示 image-20240714113101-7ery7zh.png​ 和 image-20240714113113-1fnp3y7.png​ 来展示数据结构关系)

Java 实现 (LFUCache​)

import java.util.HashMap;
import java.util.LinkedHashSet;class LFUCache {// key -> valueHashMap<Integer, Integer> keyToVal;// key -> frequencyHashMap<Integer, Integer> keyToFreq;// frequency -> keys (保持插入顺序,即时间顺序)HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;// 记录当前缓存中存在的最小频次int minFreq;// 缓存的最大容量int cap;public LFUCache(int capacity) {keyToVal = new HashMap<>();keyToFreq = new HashMap<>();freqToKeys = new HashMap<>();this.cap = capacity;this.minFreq = 0; // 初始最小频次为0}public int get(int key) {if (!keyToVal.containsKey(key)) {return -1;}// 增加 key 对应的频次increaseFreq(key);return keyToVal.get(key);}public void put(int key, int value) {if (this.cap <= 0) { // 处理容量为0或负数的边界情况return;}if (keyToVal.containsKey(key)) {// key 已存在,更新 valuekeyToVal.put(key, value);// 增加 key 对应的频次increaseFreq(key);// 注意:LFU 的更新操作仅涉及更新值和增加频率,不需要像 LRU 那样显式地“移动”} else {// key 不存在,需要插入新 key// 检查容量是否已满if (this.cap <= keyToVal.size()) {// 容量已满,需要淘汰一个 keyremoveMinFreqKey();}// 插入新的 key 和 valuekeyToVal.put(key, value);// 新 key 的初始频次为 1keyToFreq.put(key, 1);// 将新 key 加入频次为 1 的集合中freqToKeys.putIfAbsent(1, new LinkedHashSet<>());freqToKeys.get(1).add(key);// 插入新 key 后,最小频次一定是 1this.minFreq = 1;}}/* 增加 key 对应的频次 */private void increaseFreq(int key) {int freq = keyToFreq.get(key); // 获取当前频次// 更新 key 的频次keyToFreq.put(key, freq + 1);// 从旧频次的 key 集合中移除 keyLinkedHashSet<Integer> oldFreqKeys = freqToKeys.get(freq);oldFreqKeys.remove(key);// 将 key 加入新频次的 key 集合中freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());freqToKeys.get(freq + 1).add(key);// 检查旧频次的 key 集合是否为空if (oldFreqKeys.isEmpty()) {// 如果为空,则从 freqToKeys 中移除该频次条目freqToKeys.remove(freq);// 如果移除的这个频次恰好是 minFreq,则需要更新 minFreqif (freq == this.minFreq) {// 新的 minFreq 变成了 freq + 1this.minFreq++;}}}/* 淘汰一个最小频次且最旧的 key */private void removeMinFreqKey() {// 获取最小频次对应的 key 集合 (按插入顺序)LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);// 第一个元素就是最旧的 keyint deletedKey = keyList.iterator().next();// 从 key 集合中移除keyList.remove(deletedKey);// 检查移除后集合是否为空if (keyList.isEmpty()) {// 如果为空,则从 freqToKeys 中移除该频次条目freqToKeys.remove(this.minFreq);// 注意:这里不需要更新 minFreq// 因为 removeMinFreqKey() 只在 put 新元素且容量满时调用// 而 put 新元素后 minFreq 会被强制设为 1,所以旧的 minFreq 是否有效已不重要}// 从 keyToVal 和 keyToFreq 中移除该 keykeyToVal.remove(deletedKey);keyToFreq.remove(deletedKey);}
}

关键点总结 (LFU):

  • ​get​ 操作:通过 keyToVal​ 获取值,然后调用 increaseFreq​ 更新频率相关信息。

  • ​put​ 操作:

    • 若 Key 存在:更新 keyToVal​ 中的值,调用 increaseFreq​。
    • 若 Key 不存在:检查容量,若满则调用 removeMinFreqKey​ 淘汰;然后,在 keyToVal​, keyToFreq​ (设为1), freqToKeys​ (添加到freq=1的集合) 中添加新 Key,并将 minFreq​ 更新为 1。
  • ​increaseFreq​ 核心逻辑:更新 keyToFreq​,从旧频次的 LinkedHashSet​ 中移除 Key,添加到新频次的 LinkedHashSet​ (如果需要则创建)。如果旧频次集合变空且它曾是 minFreq​,则递增 minFreq​。

  • ​removeMinFreqKey​ 核心逻辑:从 freqToKeys​ 中获取 minFreq​ 对应的 LinkedHashSet​,移除其第一个元素(最旧的),并同步更新所有相关的 Map​。如果集合变空,则移除该频次条目。

三、LRU vs LFU

  • LRU: 关注最近访问时间,实现相对简单(LinkedHashMap​ 或 HashMap+DLL)。适合访问模式有较强时间局部性的场景。
  • LFU: 关注访问频率,并结合时间作为次要淘汰标准(频率相同时淘汰最旧的)。实现更复杂,需要维护频率信息和访问时序。适合需要保留高频访问数据,即使它不是最近访问的场景。

相关文章:

  • springboot 实现敏感信息脱敏
  • OpenCV 图形API(69)图像与通道拼接函数------将一个 GMat 类型的对象转换为另一个具有不同深度GMat对象函数convertTo()
  • git 修改用户名和邮箱
  • 关于常量指针和指向常量的指针
  • HTML5好看的水果蔬菜在线商城网站源码系列模板7
  • vue复习91~135
  • GPU 架构入门笔记
  • 获得ecovadis徽章资格标准是什么?ecovadis评估失败的风险
  • 【ACL系列论文写作指北07-论文标题与关键词部分怎么写】-赢在第一眼
  • 今日行情明日机会——20250428
  • leetcode128-最长连续序列
  • 【默子AI】万字长文:MCP与A2A协议详解
  • 【学习笔记】RL4LLM(三)
  • BeeWorks企业内部即时通讯软件支持国产化,已在鸿蒙系统上稳定运行
  • 云原生--核心组件-容器篇-7-Docker私有镜像仓库--Harbor
  • Linux中的计划任务
  • 第1篇:Egg.js框架入门与项目初始化
  • go语言八股文(五)
  • el-Input输入数字自动转千分位进行展示
  • LeetCode 1482. 制作 m 束花所需的最少天数
  • 我国将开展市场准入壁垒清理整治行动
  • 日中友好议员联盟代表团访问中国人民对外友好协会
  • 气温“过山车”现象未来或更频繁且更剧烈
  • 加总理:目前没有针对加拿大人的“活跃威胁”
  • 从地下金库到地上IP,看海昏汉文化“最美变装”
  • 同款瑞幸咖啡竟差了6元,开了会员仍比别人贵!客服回应