实现一个 LRU 风格的缓存类
实现一个缓存类
- 需求描述
- 豆包
- 解决思路:
- 实现代码:
- 优化1
- 1. `std::list::remove` 的时间复杂度问题
- 2. 代码复用
- 优化后的代码
- 优化说明
- 优化2
- 1. 边界条件检查
- 2. 异常处理
- 3. 代码封装性
- 4. 线程安全
- 优化后的代码示例
- 优化说明
- DeepSeek(深度思考R1):
- 代码实现
- 实现说明:
- 优化方案一:基础互斥锁版本
- 优化方案二:读写锁版本(C++17+)
- 优化分析说明:
需求描述
类似于Windows系统的 win 菜单最近打开功能模块,使用最近最少使用(LRU)的缓存淘汰策略,缓存可以设置最大容量。
get:获取缓存数据,不存在时返回 -1
put:新增或者更新缓存,当容量已满时,删除最早的数据,再插入
class UpdateCaching {
public:
UpdateCaching(int capacity){}
int get(int key){}
void put(int key ,int value){}
}
豆包
解决思路:
-
使用
std::unordered_map
存储键值对,以便快速查找和更新数据。(为啥使用 unordered_map 而不是 map) -
使用
std::list
存储键的访问顺序,以便在容量满时删除最早的数据。 -
在
put
操作中:- 检查缓存是否已满,如果已满,删除 list 中的第一个元素,并从
unordered_map
中移除对应的键值对。 - 将新元素添加到
unordered_map
中,并将键添加到list
的末尾。
- 检查缓存是否已满,如果已满,删除 list 中的第一个元素,并从
-
在
get
操作中:- 使用
unordered_map
查找键是否存在,如果存在,将键移到list
的末尾,表示最近访问过。
- 使用
实现代码:
#include <iostream>
#include <unordered_map>
#include <list>
class UpdateCaching {
private:
std::unordered_map<int, int> cache;
std::list<int> keyOrder;
int capacity;
public:
UpdateCaching(int cap) : capacity(cap) {}
int get(int key) {
if (cache.count(key) == 0) {
return -1;
}
// 将访问的键移到列表末尾,表示最近使用过
keyOrder.remove(key);
keyOrder.push_back(key);
return cache[key];
}
void put(int key, int value) {
if (cache.count(key) > 0) {
// 键已存在,更新值并将其移到列表末尾
cache[key] = value;
keyOrder.remove(key);
keyOrder.push_back(key);
return;
}
if (cache.size() >= capacity) {
// 缓存已满,移除最久未使用的元素
int oldestKey = keyOrder.front();
keyOrder.pop_front();
cache.erase(oldestKey);
}
// 插入新元素
cache[key] = value;
keyOrder.push_back(key);
}
};
int main() {
UpdateCaching cache(3);
cache.put(1, 10);
cache.put(2, 20);
cache.put(3, 30);
std::cout << cache.get(2) << std::endl; // 输出 20
cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)
std::cout << cache.get(1) << std::endl; // 输出 -1
return 0;
}
优化1
1. std::list::remove
的时间复杂度问题
在 get
和 put
方法中,使用 keyOrder.remove(key)
来移动元素到列表末尾。std::list::remove
的时间复杂度是
O
(
n
)
O(n)
O(n),因为它需要遍历整个列表来找到要删除的元素。可以通过维护一个指向每个键在列表中位置的映射,将这一操作的时间复杂度优化到
O
(
1
)
O(1)
O(1)。
2. 代码复用
get
和 put
方法中都有将键移到列表末尾的操作,可以将这一操作提取成一个单独的方法,提高代码的复用性。
优化后的代码
#include <iostream>
#include <unordered_map>
#include <list>
class UpdateCaching {
private:
std::unordered_map<int, int> cache;
std::unordered_map<int, std::list<int>::iterator> keyIterators;
std::list<int> keyOrder;
int capacity;
// 将键移到列表末尾
void moveToBack(int key) {
auto it = keyIterators[key];
keyOrder.erase(it);
keyOrder.push_back(key);
keyIterators[key] = --keyOrder.end();//更新映射容器中key对应的迭代器
}
public:
UpdateCaching(int cap) : capacity(cap) {}
int get(int key) {
if (cache.count(key) == 0) {
return -1;
}
// 将访问的键移到列表末尾,表示最近使用过
moveToBack(key);
return cache[key];
}
void put(int key, int value) {
if (cache.count(key) > 0) {
// 键已存在,更新值并将其移到列表末尾
cache[key] = value;
moveToBack(key);
return;
}
if (cache.size() >= capacity) {
// 缓存已满,移除最久未使用的元素
int oldestKey = keyOrder.front();
keyOrder.pop_front();
cache.erase(oldestKey);
keyIterators.erase(oldestKey);
}
// 插入新元素
cache[key] = value;
keyOrder.push_back(key);
keyIterators[key] = --keyOrder.end();
}
};
int main() {
UpdateCaching cache(3);
cache.put(1, 10);
cache.put(2, 20);
cache.put(3, 30);
std::cout << cache.get(2) << std::endl; // 输出 20
cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)
std::cout << cache.get(1) << std::endl; // 输出 -1
return 0;
}
优化说明
- 引入
keyIterators
映射:用于存储每个键在keyOrder
列表中的迭代器,这样在移动元素到列表末尾时,就可以直接通过迭代器删除元素,时间复杂度为 O ( 1 ) O(1) O(1)。 - 提取
moveToBack
方法:将将键移到列表末尾的操作提取成一个单独的方法,提高了代码的复用性和可读性。
通过这些优化,get
和 put
方法的时间复杂度都可以优化到
O
(
1
)
O(1)
O(1)。
优化2
1. 边界条件检查
在构造函数中可以添加对 capacity
参数的检查,避免传入不合法的值(如负数),增强代码的健壮性。
2. 异常处理
在实际应用中,std::unordered_map
和 std::list
的操作可能会抛出异常(如内存分配失败),可以考虑添加异常处理机制,让代码在异常情况下能有更好的表现。
3. 代码封装性
可以提供一些额外的接口方法,如获取缓存当前大小、检查缓存是否为空等,方便调用者使用。
4. 线程安全
如果该缓存类要在多线程环境下使用,需要添加同步机制,保证线程安全。
优化后的代码示例
#include <iostream>
#include <unordered_map>
#include <list>
#include <mutex>
class UpdateCaching {
private:
std::unordered_map<int, int> cache;
std::unordered_map<int, std::list<int>::iterator> keyIterators;
std::list<int> keyOrder;
int capacity;
mutable std::mutex mtx; // 用于线程安全的互斥锁
// 将键移到列表末尾
void moveToBack(int key) {
std::lock_guard<std::mutex> lock(mtx); // 加锁,在get或put中被调用时已经加锁了,且是同一个锁 mtx,不会再次进行真正的加锁操作
auto it = keyIterators[key];
keyOrder.erase(it);
keyOrder.push_back(key);
keyIterators[key] = --keyOrder.end();
}
public:
// 构造函数添加边界检查
UpdateCaching(int cap) : capacity(cap) {
if (cap <= 0) {
throw std::invalid_argument("Capacity must be a positive integer.");
}
}
int get(int key) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (cache.count(key) == 0) {
return -1;
}
// 将访问的键移到列表末尾,表示最近使用过
moveToBack(key);
return cache[key];
}
void put(int key, int value) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (cache.count(key) > 0) {
// 键已存在,更新值并将其移到列表末尾
cache[key] = value;
moveToBack(key);
return;
}
if (cache.size() >= capacity) {
// 缓存已满,移除最久未使用的元素
int oldestKey = keyOrder.front();
keyOrder.pop_front();
cache.erase(oldestKey);
keyIterators.erase(oldestKey);
}
// 插入新元素
cache[key] = value;
keyOrder.push_back(key);
keyIterators[key] = --keyOrder.end();
}
// 获取缓存当前大小
size_t size() const {
std::lock_guard<std::mutex> lock(mtx); // 加锁
return cache.size();
}
// 检查缓存是否为空
bool empty() const {
std::lock_guard<std::mutex> lock(mtx); // 加锁
return cache.empty();
}
};
int main() {
try {
UpdateCaching cache(3);
cache.put(1, 10);
cache.put(2, 20);
cache.put(3, 30);
std::cout << cache.get(2) << std::endl; // 输出 20
cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)
std::cout << cache.get(1) << std::endl; // 输出 -1
std::cout << "Cache size: " << cache.size() << std::endl;
std::cout << "Is cache empty? " << (cache.empty() ? "Yes" : "No") << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
在多线程环境下使用:
// 线程函数,用于向缓存中插入数据
void insertData(UpdateCaching& cache, int start, int end) {
for (int i = start; i < end; ++i) {
cache.put(i, i * 10);
}
}
// 线程函数,用于从缓存中获取数据
void getData(UpdateCaching& cache, int start, int end) {
for (int i = start; i < end; ++i) {
int value = cache.get(i);
if (value != -1) {
std::cout << "Key: " << i << ", Value: " << value << std::endl;
}
}
}
int main() {
try {
UpdateCaching cache(10);
// 创建插入数据的线程
std::thread insertThread1(insertData, std::ref(cache), 0, 5);
std::thread insertThread2(insertData, std::ref(cache), 5, 10);
// 创建获取数据的线程
std::thread getThread1(getData, std::ref(cache), 0, 5);
std::thread getThread2(getData, std::ref(cache), 5, 10);
// 等待所有线程完成
insertThread1.join();
insertThread2.join();
getThread1.join();
getThread2.join();
std::cout << "Cache size: " << cache.size() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
优化说明
- 边界条件检查:在构造函数中添加了对
capacity
参数的检查,如果传入的值不合法,会抛出std::invalid_argument
异常。 - 异常处理:通过
try-catch
块捕获可能抛出的异常,并进行相应的处理,提高了代码的健壮性。 - 代码封装性:添加了
size()
和empty()
方法,方便调用者获取缓存的当前大小和检查缓存是否为空。 - 线程安全:使用
std::mutex
实现了简单的线程安全机制,在关键操作(如get
、put
、size
、empty
等)前加锁,确保多线程环境下的操作安全。
DeepSeek(深度思考R1):
代码实现
#include <list>
#include <unordered_map>
#include <algorithm>
using namespace std;
class UpdateCaching {
private:
int max_capacity;
list<pair<int, int>> cache_list;
unordered_map<int, list<pair<int, int>>::iterator> cache_map;
public:
UpdateCaching(int capacity) {
max_capacity = max(0, capacity); // 处理非法容量值
}
int get(int key) {
auto it = cache_map.find(key);
if (it == cache_map.end()) {
return -1;
}
// 将访问的节点移动到链表头部
cache_list.splice(cache_list.begin(), cache_list, it->second);
return it->second->second;
}
void put(int key, int value) {
if (max_capacity <= 0) return; // 容量为0时不操作
auto it = cache_map.find(key);
if (it != cache_map.end()) {
// 更新已有值并移到头部
it->second->second = value;
cache_list.splice(cache_list.begin(), cache_list, it->second);
return;
}
// 插入新数据
if (cache_map.size() >= max_capacity) {
// 删除最久未使用的数据
int last_key = cache_list.back().first;
cache_map.erase(last_key);
cache_list.pop_back();
}
// 插入新数据到头部
cache_list.emplace_front(key, value);
cache_map[key] = cache_list.begin();
}
};
实现说明:
-
数据结构选择:
- 使用双向链表
list
维护访问顺序,最近访问的节点保持在链表头部 - 使用哈希表
unordered_map
实现O(1)时间复杂度的查找操作
- 使用双向链表
-
核心操作:
- get操作:
- 查找存在时,将节点移动到链表头部并返回值
- 不存在时返回-1
- put操作:
- 已存在时更新值并移动到头部
- 不存在时先检查容量,满时删除链表尾部元素
- 插入新元素到链表头部并更新哈希表
- get操作:
-
边界处理:
- 构造函数处理非法容量值(负数自动设为0)
- 容量为0时缓存不可用
- 正确处理链表空时的删除操作
-
时间复杂度:
- get和put操作的时间复杂度均为O(1)
- 链表操作使用splice实现节点移动,保证O(1)时间复杂度
在原有LRU缓存实现基础上,可以通过以下方案优化线程安全问题:
优化方案一:基础互斥锁版本
#include <list>
#include <unordered_map>
#include <mutex>
class UpdateCaching {
private:
int max_capacity;
list<pair<int, int>> cache_list;
unordered_map<int, list<pair<int, int>>::iterator> cache_map;
mutable mutex cache_mutex; // 可变的互斥锁
public:
UpdateCaching(int capacity) : max_capacity(max(0, capacity)) {}
int get(int key) {
lock_guard<mutex> lock(cache_mutex); // 自动加锁
if (auto it = cache_map.find(key); it != cache_map.end()) {
cache_list.splice(cache_list.begin(), cache_list, it->second);
return it->second->second;
}
return -1;
}
void put(int key, int value) {
lock_guard<mutex> lock(cache_mutex);
if (max_capacity <= 0) return;
if (auto it = cache_map.find(key); it != cache_map.end()) {
it->second->second = value;
cache_list.splice(cache_list.begin(), cache_list, it->second);
return;
}
if (cache_map.size() >= max_capacity) {
cache_map.erase(cache_list.back().first);
cache_list.pop_back();
}
cache_list.emplace_front(key, value);
cache_map[key] = cache_list.begin();
}
};
优化方案二:读写锁版本(C++17+)
#include <shared_mutex>
class UpdateCaching {
// ...其他成员同基础版...
mutable shared_mutex rw_mutex; // 读写锁
public:
int get(int key) {
shared_lock<shared_mutex> lock(rw_mutex); // 共享锁
// ...原有逻辑...
}
void put(int key, int value) {
unique_lock<shared_mutex> lock(rw_mutex); // 独占锁
// ...原有逻辑...
}
};
优化分析说明:
-
线程安全实现:
- 使用
std::mutex
保护所有缓存操作 - 通过
lock_guard
自动管理锁的生命周期 - 读写分离版本采用
shared_mutex
提升读多写少场景性能
- 使用
-
性能影响评估:
方案 读并发 写并发 适用场景 普通互斥锁 串行 串行 低并发通用场景 读写锁 并行读 串行写 读多写少的高并发场景 -
异常安全:
- 所有容器操作保证基本异常安全
- 使用RAII风格的锁管理确保不会死锁
-
扩展优化建议:
// 示例:分段锁优化(适用于超高并发场景) class SegmentedLRUCache { constexpr static int SEGMENTS = 16; array<mutex, SEGMENTS> segment_mutexes; array<UpdateCaching, SEGMENTS> segments; size_t get_segment(int key) { return hash<int>{}(key) % SEGMENTS; } public: int get(int key) { auto seg = get_segment(key); lock_guard<mutex> lock(segment_mutexes[seg]); return segments[seg].get(key); } // 类似实现put方法... };
建议根据实际使用场景选择优化方案:
- 常规服务:基础互斥锁方案即可满足需求
- 高频读取服务:升级到读写锁版本
- 超大规模并发:考虑分段锁或无锁队列方案
需要特别注意:线程安全实现会带来约10%-30%的性能损耗,在单线程场景中可以通过编译选项禁用锁机制(通过条件编译或模板策略)。