C++实现LRU缓存的操作方法
作者:吃小南瓜
LRU的概念
LRU(Least Recently Used,最近最少使用)是一种常用的缓存淘汰策略,主要目的是在缓存空间有限的情况下,优先淘汰那些最长时间没有被访问的数据项。LRU 策略的核心思想是:
- 缓存空间有限:缓存只能存储一定数量的数据项。
- 淘汰最不常用的数据:当缓存满时,优先淘汰那些最近最少被访问的数据项。
- 访问记录:每次数据项被访问时,都会更新其访问记录,使得最近访问的数据项保留在缓存中。
- 数据替换:当需要加载新数据项到缓存中,但缓存已满时,会根据LRU策略淘汰一个或多个数据项,为新数据项腾出空间。
- 动态调整:随着数据访问模式的变化,LRU策略可以动态调整缓存中的数据项,以适应访问模式的变化。
在实现LRU缓存时,通常会使用数据结构如哈希表和双向链表。哈希表用于快速定位缓存中的数据项,而双向链表则用于维护数据项的访问顺序。每次访问数据项时,都会将其移动到链表的头部,表示它是最近被访问的。当需要淘汰数据时,直接从链表的尾部开始淘汰即可。
LRU策略在许多场景中都非常有用,比如操作系统的页面置换、数据库的查询缓存、Web服务器的页面缓存等。它可以帮助系统更有效地利用有限的缓存资源,提高系统的整体性能。
别急,我们先学实现LRU要用的哈希表和双向链表
哈希表(unordered_map)
在C++中,unordered_map
是标准模板库(STL)中的一个关联容器,它基于哈希表的实现。它存储了键值对,允许通过键快速访问和修改值。unordered_map
提供了平均常数时间复杂度的访问、插入和删除操作。
主要特性
- 基于哈希表:通过哈希函数将键映射到存储位置,实现快速查找。
- 键不重复:每个键在容器中是唯一的。
- 无序存储:元素的存储顺序不依赖于插入顺序,因此迭代器的遍历顺序可能与插入顺序不同。
常用操作
- 构造和初始化:
unordered_map()
:创建一个空的unordered_map
。unordered_map(initializer_list<value_type>)
:使用初始化列表创建unordered_map
。
- 插入操作:
insert(value_type)
:插入一个键值对。insert(initializer_list<value_type>)
:插入多个键值对。
- 访问操作:
operator[]
:通过键访问对应的值,如果键不存在,则插入一个新元素。at(key)
:通过键访问对应的值,如果键不存在,则抛出std::out_of_range
异常。
- 查找操作:
find(key)
:查找键是否存在,返回一个迭代器。count(key)
:返回键出现的次数(对于unordered_map
总是返回 0 或 1)。
- 删除操作:
erase(it)
:删除迭代器it
指向的元素。erase(first, last)
:删除从first
到last
(不包括last
)范围内的所有元素。erase(key)
:删除指定键的所有元素。
- 大小和容量:
size()
:返回容器中元素的数量。empty()
:如果容器为空,返回true
。
- 迭代器:
begin()
:返回指向容器开始的迭代器。end()
:返回指向容器结束的迭代器。 示例代码
以下是使用 unordered_map
的一个简单示例:
#include <iostream> #include <unordered_map> int main() { // 创建一个 unordered_map,键为 int,值为 string unordered_map<int, string> umap; // 插入元素 umap[1] = "one"; umap[2] = "two"; umap[3] = "three"; // 访问并打印元素 for (const auto& pair : umap) { std::cout << pair.first << ": " << pair.second << std::endl; } // 访问特定键的值 try { std::cout << "Value for key 2: " << umap.at(2) << std::endl; } catch (const std::out_of_range& e) { std::cerr << e.what() << std::endl; } // 查找键是否存在 auto it = umap.find(3); if (it != umap.end()) { std::cout << "Key 3 found, value: " << it->second << std::endl; } // 删除元素 umap.erase(2); std::cout << "After erasing key 2:" << std::endl; for (const auto& pair : umap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
输出:
1: one
2: two
3: three
Value for key 2: two
Key 3 found, value: three
After erasing key 2:
1: one
3: three
在这个示例中:
- 创建了一个
unordered_map
并插入了一些键值对。 - 遍历并打印了
unordered_map
中的所有元素。 - 使用
at()
方法安全地访问特定键的值。 - 使用
find()
方法查找键是否存在,并访问对应的值。 - 使用
erase()
方法删除了键为 2 的元素,并再次打印了剩余的元素。
双向链表(list)
在C++中,list
是标准模板库(STL)中的一个容器类,它提供了双向链表的实现。与数组或向量(vector
)不同,list
允许在任意位置高效地插入和删除元素,而不需要移动其他元素。
以下是 list
的一些主要特性和常用操作:
特性
- 双向链表:每个元素都是链表中的一个节点,可以从前向后或从后向前遍历。
- 动态大小:
list
的大小可以根据需要动态变化,不需要预先定义大小。 - 插入和删除操作:可以在常数时间内在任意位置插入或删除元素,不需要像
vector
那样移动其他元素。
常用操作
- 插入操作:
push_front(value)
:在链表头部插入一个元素。push_back(value)
:在链表尾部插入一个元素。insert(position, value)
:在指定位置插入一个元素。insert(position, n, value)
:在指定位置插入n
个相同的元素。insert(position, first, last)
:在指定位置插入一个范围内的元素。
- 删除操作:
pop_front()
:删除链表头部的元素。pop_back()
:删除链表尾部的元素。erase(position)
:删除指定位置的元素。erase(first, last)
:删除从first
到last
(不包括last
)范围内的所有元素。
- 访问操作:
front()
:返回链表头部的元素。back()
:返回链表尾部的元素。
- 迭代器:
begin()
:返回指向链表头部的迭代器。end()
:返回指向链表尾部的迭代器。
- 大小和容量:
size()
:返回链表中元素的数量。empty()
:如果链表为空,返回true
。
示例代码
以下是使用 list
的一个简单示例:
#include <iostream> #include <list> int main() { list<int> myList; // 向链表中添加元素 myList.push_back(10); myList.push_back(20); myList.push_front(5); // 访问并打印链表中的元素 for (list<int>::iterator it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " "; } std::cout << std::endl; // 删除头部元素 myList.pop_front(); std::cout << "After popping front: "; for (auto it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " "; } std::cout << std::endl; // 删除尾部元素 myList.pop_back(); std::cout << "After popping back: "; for (auto it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " "; } std::cout << std::endl; return 0; }
输出:
5 10 20
After popping front: 10 20
After popping back: 10
在这个示例中,我们创建了一个 list
并添加了一些整数元素。然后,我们遍历并打印链表中的元素,删除头部和尾部的元素,并再次打印链表中的元素。
到这里,你已经掌握实现LRU缓存的两个条件了,马上你就要成功了!!!
真的,不信你往下看!
LRU缓存(C++)
#include <iostream> #include <list> #include <unordered_map> // 使用 using namespace std; 来简化代码,避免重复书写 std:: 前缀 using namespace std; // LRUCache 类定义 class LRUCache { private: int capacity; // 缓存的容量 list<int> keys; // 使用双向链表存储键,保持访问顺序 unordered_map<int, pair<int, list<int>::iterator>> cache; // 存储键值对和对应的链表迭代器 public: // 构造函数,初始化缓存容量 LRUCache(int capacity) : capacity(capacity) {} // 获取缓存中键对应的值 int get(int key) { auto it = cache.find(key); if (it == cache.end()) { return -1; // 如果键不存在,返回 -1 } // 更新访问顺序,将该键移动到链表头部 keys.erase(it->second.second); keys.push_front(key); it->second.second = keys.begin(); return it->second.first; // 返回键对应的值 } // 插入或更新缓存中的键值对 void put(int key, int value) { if (cache.size() >= capacity && cache.find(key) == cache.end()) { // 如果缓存已满且键不存在,淘汰最不常用的键(链表尾部的键) auto last = keys.back(); cache.erase(cache.find(last)); keys.pop_back(); } // 插入或更新键值对,并更新访问顺序 cache[key] = {value, keys.insert(keys.begin(), key)}; } }; int main() { // 创建一个容量为 2 的 LRU 缓存 LRUCache cache(2); // 插入键值对 (1, 1) cache.put(1, 1); // 访问键 1,输出其值 cout << "get(1) = " << cache.get(1) << endl; // 返回 1 // 插入键值对 (2, 2) cache.put(2, 2); // 访问键 2,输出其值 cout << "get(2) = " << cache.get(2) << endl; // 返回 2 // 插入键值对 (3, 3),由于缓存已满,键 1 被淘汰 cache.put(3, 3); // 访问键 1,由于已被淘汰,返回 -1 cout << "get(1) = " << cache.get(1) << endl; // 返回 -1 // 访问键 3,输出其值 cout << "get(3) = " << cache.get(3) << endl; // 返回 3 // 插入键值对 (4, 4),由于缓存已满,键 2 被淘汰 cache.put(4, 4); // 访问键 1,由于已被淘汰,返回 -1 cout << "get(1) = " << cache.get(1) << endl; // 返回 -1 // 访问键 3,输出其值 cout << "get(3) = " << cache.get(3) << endl; // 返回 3 // 访问键 2,由于已被淘汰,返回 -1 cout << "get(2) = " << cache.get(2) << endl; // 返回 -1 // 访问键 4,输出其值 cout << "get(4) = " << cache.get(4) << endl; // 返回 4 return 0; }
这段代码首先定义了一个 LRUCache
类,该类使用 unordered_map
和 list
来实现 LRU 缓存机制。get
方法用于获取缓存中的值,如果键存在,则返回其值并更新访问顺序;如果键不存在,则返回 -1。put
方法用于插入或更新缓存中的键值对,如果缓存已满,则淘汰最不常用的键(链表尾部的键)。在 main
函数中,创建了一个 LRUCache
对象并进行了一些操作来演示其功能。
什么?看不懂?没关系,结合下面的过程看,你应该就明白了!
初始化状态
Cache: {} Keys: []
执行 cache.put(1, 1)
Cache: {1: (1, it1)} Keys: [1]
执行 cache.put(2, 2)
Cache: {1: (1, it1), 2: (2, it2)} Keys: [2, 1] (2 最近使用,1 最少使用)
执行 cache.put(3, 3)
缓存已满,淘汰键 1
Cache: {2: (2, it2), 3: (3, it3)} Keys: [3, 2] (3 最近使用,2 次之)
执行 cache.get(1)
键 1 不存在,返回 -1
Cache: {2: (2, it2), 3: (3, it3)} Keys: [3, 2]
执行 cache.get(3)
键 3 存在,返回 3,并更新为最近使用
Cache: {2: (2, it2), 3: (3, it3)} Keys: [3, 2]
执行 cache.put(4, 4)
缓存已满,淘汰键 2
Cache: {3: (3, it3), 4: (4, it4)} Keys: [4, 3] (4 最近使用,3 次之)
执行 cache.get(1)
键 1 不存在,返回 -1
Cache: {3: (3, it3), 4: (4, it4)} Keys: [4, 3]
执行 cache.get(3)
键 3 存在,返回 3,并更新为最近使用
Cache: {3: (3, it3), 4: (4, it4)} Keys: [3, 4]
执行 cache.get(2)
键 2 不存在,返回 -1
Cache: {3: (3, it3), 4: (4, it4)} Keys: [3, 4]
执行 cache.get(4)
键 4 存在,返回 4,并更新为最近使用
Cache: {3: (3, it3), 4: (4, it4)} Keys: [4, 3]
至此,你就算没有台明白,也一定了解LRU了。收藏可以方便下次巩固哦!!!!
到此这篇关于C++实现LRU缓存的文章就介绍到这了,更多相关C++ LRU缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!