缓存淘汰算法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. 关键点解析
LinkedList
维护访问顺序,头部是最新访问的数据HashMap
提供O(1)时间的键值访问- 访问数据时通过
updateAccess
方法更新位置 - 容量满时从链表尾部淘汰最久未使用的数据
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. 关键点解析
- 使用三个核心数据结构:
keyToValue
:存储键值对keyToFrequency
:记录访问频率frequencyToKeys
:维护相同频率下的访问顺序
LinkedHashSet
保证同频率下的时序性minFrequency
变量高效定位淘汰目标- 新插入数据的初始频率总是1
4. 复杂度分析
- 时间复杂度:
- get操作:O(1)平均
- put操作:O(1)平均
- 空间复杂度:O(capacity)
四、两种算法对比
特性 | LRU | LFU |
---|---|---|
淘汰原则 | 最近最少使用 | 使用频率最低 |
数据结构 | 哈希表+双向链表 | 三重映射结构 |
时间复杂度 | O(1)访问和插入 | O(1)平均时间复杂度 |
优势场景 | 突发流量、时间局部性强的访问模式 | 稳定流量、长期热点数据 |
实现复杂度 | 简单 | 较复杂 |
五、生产环境建议
-
选择依据:
LRU
适合时间局部性强的场景LFU
适合需要识别长期热点的场景
-
优化建议:
- 使用
Caffeine
等成熟缓存库 - 高并发场景采用分片缓存策略
- 使用
-
扩展思考:
- 可结合
TTL
过期策略 - 考虑
LRU-K
等混合算法
- 可结合