目录
实验目的及内容
设计实现一个LRU缓存淘汰算法( Least Recently Used,最近最少使用原则:若缓存满了,首先删除最久没有使用过的数据),要求算法时间复杂度为O(1)。(提示:双向链表+哈希表)
解题思路
1.LRU算法的基本思想(图解)
LRU 缓存淘汰算法是一种常用淘汰策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是最「有用的」,很久都没用过的数据应该是 无用的,内存满了就优先删那些很久没用过的数据。
所以,若是用链表来存储缓存数据,应有如下规则:
- 每次新插入数据的时候将新数据插到链表的头部;
- 每次缓存命中(即数据被访问),则将数据移到链表头部;
- 当链表满的时候,就将链表尾部的数据丢弃。
2.数据结构设计
这题实际上需要我们自己来设计数据结构“哈希链表”:这个数据结构实现了两个基本 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。!注意,存入get 和 查找put 必须都是 O(1)
的时间复杂度!
基于以上的需求分析,我们会有以下思路:
用双向链表来存储所有“缓存数据”,方便修改和删除(时间复杂度为O(1));
用哈希表来实现对双向链表中数据的快速查找;为了方便,可以用C++的<unordered_map>模板容器实现哈希表结构,为此,可以定义如下模板:unordered_map<int, ListNode*> cache,其中key是int型,表示用户序号,而值是ListNode*指针,指向存储该用户缓存信息的链节点地址;
最后将哈希表和双向链表结合,构成“哈希链表”:借助哈希表赋予链表快速查找的特性:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。既能实现O(1)的高效查找,又可以实现高效的删除;
实验代码及注释
特别注意:当缓存容量已满,我们不仅仅要删除最后一个 ListNode 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 ListNode 得到。所以我们的ListNode节点不仅要存储value还要存储对应的key值!
#include <iostream>
#include <unordered_map>
using namespace std;
// 双向链节点
struct ListNode
{
int key; // 键
int value; // 值
ListNode *prev;
ListNode *next;
ListNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
// 定义LRU缓存类
class LRUCache
{
private:
int capacity; // 缓存容量
unordered_map<int, ListNode *> map; // 哈希表,用于快速查找结点
ListNode *head; // 双向链表的头结点
ListNode *tail; // 双向链表的尾结点
// 将结点添加到链表头部
void addToHead(ListNode *node)
{
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
// 删除链表中的结点
void removeNode(ListNode *node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
// delete node;
}
// 将结点移动到链表头部(每次用户访问)
void moveToHead(ListNode *node)
{
removeNode(node);
addToHead(node);
}
public:
// 构造函数,初始化LRU缓存对象,创建一个空的双向链表
LRUCache(int capacity)
{
this->capacity = capacity;
head = new ListNode(-1, -1);
tail = new ListNode(-1, -1);
head->next = tail;
tail->prev = head;
}
// 析构函数
~LRUCache()
{
// 循环遍历cache中的每个键值对,使用delete删除每个节点并释放其内存
for (auto &pair : map) // auto& 是范围循环中的类型推断机制,用于自动推断迭代变量 pair
{
delete pair.second; // pair.first是键,pair.second是值(表示缓存结点的指针)
}
// 释放cache中的每个ListNode对象的内存
map.clear(); // 这个函数仅仅清楚元素的值,指针所占用的内存没有释放!
// 清空整个unordered_map,以确保所有ListNode对象的内存都被正确释放
delete head;
delete tail;
}
// 添加或更新缓存
void put(int key, int value)
{
if (map.find(key) == map.end())
{ // 从头到尾遍历链表,如果返回 map.end(),表示未找到指定的键
if (map.size() >= capacity)
{ // 缓存已满,删除最久未使用的结点(尾结点的前一个节点)
ListNode *lastNode = tail->prev;
map.erase(lastNode->key); // 从哈希表 map 中删除指定的键 key 对应的键值对
removeNode(lastNode); // 从双向链表移除这个结点
delete lastNode;
}
// 创建新节点
ListNode *newNode = new ListNode(key, value);
map[key] = newNode; // 将键值对(key, newNode)插入或更新到unordered_map容器 map 中
addToHead(newNode);
}
else
{ // 缓存中已存在该键,更新值并将结点移到链表头部,更新最近访问的结点
ListNode *node = map[key]; // 从哈希表 map 中获取给定键 key 对应的结点指针
node->value = value; // 更新该键对应结点的值
moveToHead(node);
}
}
// 获取缓存中的值
int get(int key)
{
// map.find(key) 的返回值是一个迭代器,它指向一个键为 key 的结点(键值对)
if (map.find(key) == map.end())
{
return -1; // 表示缓存中不存在该键
}
ListNode *node = map[key];
moveToHead(node); // 将结点移动到链表头部
return node->value;
}
// 打印缓存内容
void print()
{
ListNode *p;
for (p = head->next; p->next != tail; p = p->next)
{
cout << "(" << p->key << "," << p->value << ")->";
}
cout << "(" << p->key << "," << p->value << ")";
}
};
int main()
{
// 定义一个容量为5的LRUCache
LRUCache cache(5);
cout << "the capacity of cache is 5!\n";
// 添加用户1至用户4的缓存信息
cache.put(1, 100);
cache.put(2, 200);
cache.put(3, 300);
cache.put(4, 400);
cout << "the current cache is:";
cache.print();
// 访问用户1
cout << "\n\n访问用户1的缓存信息\n";
cout << "用户1的值: " << cache.get(1) << endl;
cout << "the current cache is:";
cache.print();
// 先后添加用户5和用户6的缓存,此时用户2将被淘汰
cout << "\n\n加入用户5的缓存信息\n";
cache.put(5, 500);
cout << "the current cache is:";
cache.print();
cout << "\n\n加入用户6的缓存信息\n";
cache.put(6, 600);
cout << "the current cache is:";
cache.print();
// 访问用户2,应返回-1
cout << "\n\n访问用户2的缓存信息\n";
cout << "用户2的值: " << cache.get(2) << endl;
// 访问用户6
cout << "\n访问用户6的缓存信息\n";
cout << "用户6的值: " << cache.get(6) << endl;
return 0;
}
测试结果截图
心得体会
这题实际上需要我们自己来设计数据结构“哈希链表”,借助哈希表赋予链表快速查找的特性:可以快速查找某个 key 是否存在缓存(链表)中,同时借助链表实现快速删除、添加节点。既能实现O(1)的高效查找,又可以实现高效的删除;