算法 ------ LRU算法
如果大家已经学习过了Cache的替换算法和页面置换算法,大家一定对LRU(Least Recently Used,最近最少使用)不陌生,我们今天来研究下这个算法:
这里有一个例子:
LRU
LRU(Least Recently Used,最近最少使用)算法是一种常用的缓存淘汰策略,用于在缓存空间有限的情况下决定哪些数据应该被保留,哪些数据应该被移除 LRU算法的基本理念是:如果某数据在最近一段时间内没有被访问,那么在未来被访问的可能性也比较低。反之,如果某数据被频繁访问,那么它应当被保留在缓存中。
LRU算法的工作原理:
- 缓存初始化:当缓存初始化时,它是空的。
- 数据访问:
- 如果请求的数据已经在缓存中,称为缓存命中(Hit),则更新该数据项的访问状态,表明它最近被使用过。
- 如果请求的数据不在缓存中,称为缓存未命中(Miss),则需要从主存或其他存储中加载数据到缓存。
- 数据淘汰:
- 当缓存已满,而新的数据需要加入缓存时,LRU算法会选择最近最少使用的数据项进行淘汰,以便为新数据腾出空间。
- "最近最少使用"的定义是:在当前时刻,从上次访问到现在时间间隔最长的数据。
实现方法:
LRU算法可以通过多种数据结构来实现,其中最常见的是使用双向链表和哈希表的组合:
- 双向链表:用于维护数据项的访问顺序,最新访问的数据放在链表头部,最久未访问的数据放在链表尾部。
- 哈希表:用于快速查找数据项在双向链表中的位置。
当数据被访问时,它从链表中的当前位置移动到链表头部。当缓存满时,链表尾部的数据项被移除。
性能考虑:
LRU算法虽然直观且有效,但在某些情况下可能会有性能开销,尤其是当数据集非常大时,维护链表的插入和删除操作可能会成为瓶颈。此外,如果数据访问模式中存在大量突发性的随机访问,LRU算法可能无法很好地预测哪些数据是真正需要保留在缓存中的。
尽管如此,LRU仍然是许多缓存系统中首选的淘汰策略,因为它在大多数情况下能够提供较好的命中率和性能。在软件和硬件缓存管理中,LRU算法都有广泛应用。例如,在Web服务器缓存、数据库查询缓存、CPU缓存和虚拟内存管理系统中都能见到它的身影。
模拟过程
我们这里用unordered_map和list来模拟:
cpp
#pragma once
#include<iostream>
#include<unordered_map>
#include<list>
using namespace std;
class LRUCache {
public:
LRUCache(int capacity)
{
}
int get(int key)
{
}
void put(int key, int value)
{
}
private:
size_t _capacity; //容量
//查询
unordered_map<int ,list<pair<int,int>>::iterator> _LRUMap;
//插入删除
list<pair<int, int>> _LRUList;
};
unordered_map可以帮助我们查询是O(1)的时间复杂度,list帮助我们模拟过程,这里我们unordered_map的第二个键值对是list的迭代器,这个方便我们直接修改顺序是O(1)的操作:
我们来看看:
我们按照这个过程来模拟:
cpp
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
这个时候执行了查询操作:
cpp
lRUCache.get(1); // 返回 1
接下来我们放入了3,3:
cpp
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
以此类推,我们可以得出代码:
cpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <list>
using namespace std;
class LRUCache {
public:
// 构造函数,初始化缓存容量
LRUCache(int capacity) : _capacity(capacity) {}
// 获取缓存中的值,如果存在则更新其位置至最近使用
int get(int key) {
// 查找键值对应的迭代器
auto ret = _LRUMap.find(key);
if (ret != _LRUMap.end()) { // 如果找到了键值
list<pair<int, int>>::iterator it = ret->second;
// 将找到的元素移动到列表的前端,表示最近被使用
_LRUList.splice(_LRUList.begin(), _LRUList, it);
// 返回值
return it->second;
} else {
// 如果没有找到,返回-1
return -1;
}
}
// 插入或更新键值对
void put(int key, int value) {
// 查找键值对应的迭代器
auto ret = _LRUMap.find(key);
if (ret == _LRUMap.end()) { // 如果没找到,即键值不存在
// 如果缓存已满
if (_capacity == _LRUList.size()) {
// 删除最旧的元素(列表的最后一个元素)
_LRUMap.erase(_LRUList.back().first);
_LRUList.pop_back();
}
// 插入新的键值对到列表前端
_LRUList.push_front(make_pair(key, value));
// 更新或添加键值对应的迭代器到哈希表
_LRUMap[key] = _LRUList.begin();
} else {
// 如果键值已存在,更新值并移动到列表前端
list<pair<int, int>>::iterator it = ret->second;
it->second = value; // 更新值
// 将元素移动到列表前端
_LRUList.splice(_LRUList.begin(), _LRUList, it);
}
}
// 打印缓存内容
void Print() {
for (auto e : _LRUList) {
cout << "key值: " << e.first << " value值: "
<< e.second << endl;
}
cout << endl;
}
private:
size_t _capacity; // 缓存的最大容量
// 用于快速查找键值对应的迭代器
unordered_map<int, list<pair<int, int>>::iterator> _LRUMap;
// 存储键值对的有序列表,用于维护最近使用的顺序
list<pair<int, int>> _LRUList;
};
splice函数
splice
是C++标准模板库(STL)中容器(如std::list
,std::forward_list
,std::deque
)的一个成员函数,用于在容器之间或容器内部移动元素。splice
函数允许你将一个容器中的元素或一组连续的元素无缝地插入到另一个相同类型的容器的指定位置,而无需复制或构造元素。这对于需要高效地重新组织元素顺序的情况非常有用。
对于std::list
和std::forward_list
对于std::list
和std::forward_list
,splice
的用法如下:
基本语法:
cpp
void splice(position, x);
void splice(position, x, iterator i);
void splice(position, x, iterator first, iterator last);
position
:在当前容器中插入元素的位置,对于std::list
,可以是iterator
或const_reference
;对于std::forward_list
,总是before_begin()
。x
:源容器,必须与当前容器具有相同的类型。i
:源容器中的单个元素迭代器。first
,last
:源容器中元素范围的迭代器。
功能描述:
splice(position, x);
:将x
容器中的所有元素移动到当前容器的position
位置之前。splice(position, x, i);
:将x
容器中由i
指向的单个元素移动到当前容器的position
位置之前。splice(position, x, first, last);
:将x
容器中由[first, last)
区间定义的元素序列移动到当前容器的position
位置之前。
示例:
假设我们有两个std::list<int>
容器,list1
和list2
,我们想把list2
中的元素5
移动到list1
的开始位置:
cpp
std::list<int> list1{1, 2, 3, 4};
std::list<int> list2{5, 6, 7, 8};
auto it = list2.find(5);
list1.splice(list1.begin(), list2, it);
现在list1
看起来应该是{5, 1, 2, 3, 4}
,而list2
应该是{6, 7, 8}
。
注意事项:
- 移动操作是常数时间复杂度的,因此
splice
非常高效。- 被移动的元素将从源容器中移除。
- 如果两个容器共享同一个分配器(例如,它们是同一个容器的不同部分),
splice
操作不会抛出异常。
对于std::deque
,splice
的用法与上述略有不同,因为std::deque
不允许在中间插入或删除元素,只能在两端进行。因此,std::deque
的splice
只接受before_begin()
和end()
作为插入位置,而且只能从另一个std::deque
中移动元素到当前std::deque
的开头或结尾。
总之,splice
是一个强大的工具,可以高效地重新组织容器中的元素,特别是在需要移动大量元素或避免不必要的元素复制时。
具体更多的可以查看官网: