文章目录
-
- 算法概述
- 核心思想
- 数据结构设计
-
- [1. 双向链表 (std::list)](#1. 双向链表 (std::list))
- [2. 哈希表 (std::unordered_map)](#2. 哈希表 (std::unordered_map))
- 代码实现解析
-
- 关键方法详解
-
- [1. 数据查询方法 `get()`](#1. 数据查询方法
get()) - [2. 数据插入/更新方法 `put()`](#2. 数据插入/更新方法
put())
- [1. 数据查询方法 `get()`](#1. 数据查询方法
- [3. 调试输出方法](#3. 调试输出方法)
- 时间复杂度分析
- 关键技术与优化
-
- [1. 移动语义优化](#1. 移动语义优化)
- [2. 链表节点移动](#2. 链表节点移动)
- [3. 迭代器稳定性](#3. 迭代器稳定性)
- 应用场景
-
- [1. 数据库缓存](#1. 数据库缓存)
- [2. 操作系统](#2. 操作系统)
- [3. Web服务](#3. Web服务)
- [4. 编程框架](#4. 编程框架)
- 完整测试示例
- 总结
算法概述
LRU(Least Recently Used)最近最少使用算法是一种常用的缓存淘汰策略。当缓存空间不足时,它会优先淘汰最久未被访问的数据,保留最近被访问的数据。
核心思想
LRU算法的核心思想是:如果一个数据在最近一段时间没有被访问到,那么它在将来被访问的可能性也很小。基于这种"局部性原理",LRU算法将最近访问的数据放在缓存中,淘汰长时间未被访问的数据。
数据结构设计
1. 双向链表 (std::list)
- 作用:维护数据的访问顺序
- 特点 :
- 头部表示最近访问的数据(MRU - Most Recently Used)
- 尾部表示最久未访问的数据(LRU - Least Recently Used)
- 支持在任意位置快速插入和删除(O(1)时间复杂度)
2. 哈希表 (std::unordered_map)
- 作用:提供快速的数据查找能力
- 特点 :
- 键(Key):缓存数据的键
- 值(Value):指向链表中对应节点的迭代器
- 查找时间复杂度:O(1)
代码实现解析
cpp
#include <iostream>
#include <unordered_map>
#include <list>
template<typename K, typename V>
class LRUCache {
public:
// 构造函数,初始化缓存容量
explicit LRUCache(size_t capacity) : cap_(capacity) {}
// 禁用拷贝构造和赋值操作
LRUCache(const LRUCache&) = delete;
LRUCache& operator=(const LRUCache&) = delete;
private:
size_t cap_; // 缓存容量
std::list<std::pair<K, V>> item_; // 双向链表,存储键值对
std::unordered_map<K, typename std::list<std::pair<K, V>>::iterator> index_; // 哈希索引
};
关键方法详解
1. 数据查询方法 get()
cpp
bool get(const K& key, V& out) {
// 在哈希表中查找键
auto it = index_.find(key);
if (it == index_.end()) {
return false; // 未找到,缓存未命中
}
// 缓存命中:获取值并移动到链表头部
out = it->second->second;
item_.splice(item_.begin(), item_, it->second);
return true;
}
工作流程:
- 在哈希表中查找键是否存在
- 如果不存在,返回
false(缓存未命中) - 如果存在:
- 通过迭代器获取对应的值
- 使用
splice()方法将节点移动到链表头部 - 返回
true(缓存命中)
2. 数据插入/更新方法 put()
cpp
void put(const K& key, V value) {
auto it = index_.find(key);
if (it != index_.end()) {
// 键已存在:更新值并移动到头部
it->second->second = std::move(value);
item_.splice(item_.begin(), item_, it->second);
return;
}
// 键不存在:检查是否需要淘汰数据
if (item_.size() == cap_) {
// 缓存已满,淘汰尾部数据
auto& old = item_.back();
index_.erase(old.first); // 从哈希表删除
item_.pop_back(); // 从链表删除
}
// 插入新数据到头部
item_.emplace_front(key, std::move(value));
index_[item_.front().first] = item_.begin(); // 更新哈希表索引
}
工作流程:
- 检查键是否已存在
- 如果存在:更新值并移动到链表头部
- 如果不存在:
- 检查缓存是否已满
- 如果已满,淘汰链表尾部的数据(从链表和哈希表中删除)
- 将新数据插入链表头部
- 在哈希表中建立新索引
3. 调试输出方法
cpp
void debug_print() {
std::cout << "[MRU->LRU]";
for (auto& p : item_) {
std::cout << "(" << p.first << "," << p.second << ")";
}
std::cout << std::endl;
}
时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 查询 (get) | O(1) | 哈希表查找 + 链表节点移动 |
| 插入/更新 (put) | O(1) | 哈希表查找 + 可能的淘汰 + 链表插入 |
| 空间复杂度 | O(n) | n为缓存容量 |
关键技术与优化
1. 移动语义优化
cpp
it->second->second = std::move(value);
item_.emplace_front(key, std::move(value));
使用 std::move() 避免不必要的拷贝操作,提高性能。
2. 链表节点移动
cpp
item_.splice(item_.begin(), item_, it->second);
std::list::splice() 可以在常数时间内将节点从一个位置移动到另一个位置,这是实现LRU算法的关键。
3. 迭代器稳定性
std::list 的迭代器在插入和删除操作(除了被删除的元素)后仍然保持有效,这保证了哈希表中存储的迭代器的有效性。
应用场景
1. 数据库缓存
- MySQL的查询缓存
- Redis的键值缓存
2. 操作系统
- 页面置换算法
- 文件系统缓存
3. Web服务
- HTTP缓存
- CDN内容分发
4. 编程框架
- 线程池任务缓存
- 连接池管理
完整测试示例
cpp
int main() {
// 创建容量为3的LRU缓存
LRUCache<int, std::string> cache(3);
// 插入初始数据
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
cache.debug_print(); // 输出: [MRU->LRU](3,three)(2,two)(1,one)
// 查询数据(会改变访问顺序)
std::string out;
if (cache.get(2, out)) {
std::cout << "get 2:" << out << std::endl; // 输出: get 2:two
}
cache.debug_print(); // 输出: [MRU->LRU](2,two)(3,three)(1,one)
// 插入新数据(触发淘汰)
cache.put(4, "four");
cache.debug_print(); // 输出: [MRU->LRU](4,four)(2,two)(3,three)
return 0;
}
总结
LRU缓存算法通过结合哈希表的快速查找和双向链表的顺序维护,实现了高效的缓存管理。这种设计模式在需要快速访问最近使用数据的场景中非常有用,是现代计算机系统中不可或缺的基础组件之一。
优点:
- 时间复杂度优秀(O(1))
- 符合局部性原理
- 实现相对简单
缺点:
- 需要维护额外的数据结构
- 内存开销较大(哈希表+链表)
- 对于某些特殊访问模式可能不是最优