博主介绍:程序喵大人
- 35- 资深C/C++/Rust/Android/iOS客户端开发
- 10年大厂工作经验
- 嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手
- 《C++20高级编程》《C++23高级编程》等多本书籍著译者
- 更多原创精品文章,首发gzh,见文末
- 👇👇记得订阅专栏,以防走丢👇👇
😉C++基础系列专栏
😃C语言基础系列专栏
🤣C++大佬养成攻略专栏
🤓C++训练营
👉🏻个人网站
最近是校招实习面试高峰期,训练营中很多同学都在面试中,有些同学甚至被大厂要求手撕LFU,觉得很离谱。
但其实手撕LFU在面试中已经不少见了,手撕LFU、手撕LRU,在现在的面试中都很常见,大家一定要掌握,平时可以多练几遍。
对应的力扣链接如下:
- https://leetcode.cn/problems/lfu-cache/
- https://leetcode.cn/problems/lru-cache/
相关LRU题解如下:
下面通过一个例子,来给大家说一下 LRU 的概念。
假设你是一位图书管理员,你需要管理一个热门书籍借阅专区,空间有限,只能存放一定数量的书籍。读者借阅书籍后需归还,当专区满了,又有新的热门书籍要上架时,你会怎么做呢?
你大概会去查看借阅记录,看哪些书籍被频繁借阅,频繁被借阅(相当于被访问)的书籍会继续留在专区,而那些很久都没有读者借阅(长时间未被访问 )的书籍,会将其从专区移除,放到普通书架,把空间让给新的热门书籍。
上面这个例子,就是我们常说的 LRU 缓存淘汰算法。
既然知道了 LRU 原理,下面我们来看一下题目要求
那我们来拆解一下,需要做哪些工作
- 设计一个数据结构,用来存储元素
- 维护数据结构里面的元素序列,能够做到需要腾出空间时,可以快速逐出最久未使用的关键字
- 能够快速的通过 key 获取 value,也就是做到随机访问
需求已经很明确了,那我们此时应该选择什么数据结构呢?因为需要快速获取 value,并且要 put key 和 value,那么数据结构肯定要有 HashMap。
在 LRU 算法里,我们要维护元素的访问顺序,每次访问一个节点,无论是新节点还是已有节点,都要把它移到有序序列的头部,以此表示它是最新被访问的。
这个有序序列会始终保持从头部到尾部,节点未被访问的时间依次递增。也就是说,序列头部的节点是刚刚被访问过的,而尾部的节点则是最久未被访问的,在需要淘汰元素时,就优先淘汰尾部节点。
以上所述,我们可以使用 **哈希表+链表 **来完成我们的需求。
那应该使用单向链表,还是双向链表呢?
移动节点到链表头部或删除链表尾部节点,都需先删除目标节点。
寻找后继节点时,单双向链表均可通过next
指针在 O (1) 时间完成;但寻找前驱节点,单向链表需从头遍历,耗时 O (n),双向链表则能借前向指针在 O (1) 时间找到。因此,为保证操作均在 O (1) 时间内完成,故应选择双向链表,本质是以空间换时间。
好了,我们来看一下,具体是如何存储元素的呢?
从上图可知,我们的 key 为 int,value 为双向链表的节点
好啦,现在我们已经清晰知道了,应该如何设计数据结构,我们进一步根据题目,来了解需求
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存
解析: HashMap 需要有 size,并初始化 map 的 key 为 int,value 为双向链表的节点
int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
解析: 如果不存在返回 -1,如果存在,则将该 value 返回,并且将该节点,移到双向链表的头部,移到链表头部的操作我们可以分两步进行
第一步:将节点从当前位置删除
第二步:在链表头部 add 该节点
具体操作如下
这样就实现了,将某节点,移动到头部的操作
void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
并将其移动到链表头部;如果不存在,则向缓存中插入该组key-value
同时在链表头部插入该节点。如果插入操作导致关键字数量会超过capacity
,则应该 逐出 最久未使用的关键字后再插入
解析:这里有两点需要注意,第一点,put 时,假设该节点存在,则需要将其放到头节点。第二点如果超出缓存容量,则需要先删除节点,再在头部插入新节点。
整体思路已经了解,我们来看代码吧
注:代码中也有详细的解析,请认真阅读代码
class LRUCache {
private:
// 链表的节点结构体,因为是双向链表,需要有 perv,next,value,key,
// 这里有人问了,为什么还需要添加 key 呢?
// 因为删去最近最少使用的键值对时,要删除链表的尾节点
// 如果节点中没有存储 key,那么就无法知道,被删除的是那个节点,也无法删除 map 中对应的 key-value
// 此时,我们是先确定要删除的链表节点,再去 map 中删除对应的 key-value
struct DouLink {
int key;
int value;
DouLink* prev;
DouLink* next;
DouLink() : key(0), value(0), prev(nullptr), next(nullptr) {}
DouLink(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
DouLink* head; // 虚拟头节点
DouLink* tail; // 虚拟尾节点,帮助我们来完成头插法和尾部删除节点
int capacity; // map 的容量
int size; // 当前节点数目
// 我们不要求有序,所以使用 unordered_map 即可,提升性能
std::unordered_map<int, DouLink*> map;
public:
// 构造函数初始化,只有虚拟头尾节点的双向链表,并配置,缓存容量
LRUCache(int capacity) : capacity(capacity), size(0) {
head = new DouLink();
tail = new DouLink();
head->next = tail;
tail->prev = head;
}
// 释放
~LRUCache() {
DouLink* current = head;
while (current != nullptr) {
DouLink* nextNode = current->next;
delete current;
current = nextNode;
}
}
// 获取节点内容
int get(int key) {
auto it = map.find(key);
// 未发现返回 -1,符合题目要求
if (it == map.end()) {
return -1;
}
// 访问节点
DouLink* temp = it->second;
// 将最新被访问的节点,移到链表头部
moveHead(temp);
// 返回值
return temp->value;
}
// put 有两种情况,原先是否存该值
void put(int key, int value) {
auto it = map.find(key);
// 不存在
if (it == map.end()) {
// 增加新元素前,判断是否需要清理空间(链表尾部节点,长时间未被访问节点)
if (size == capacity) {
DouLink* newNode = removeTail();
map.erase(newNode->key);
delete newNode;
--size;
}
// 初始化节点,并执行插入到链表头部
DouLink* test = new DouLink(key, value);
addHead(test);
// map 也执行对应的插入 key-value
map[key] = test;
++size; // 记录当前存储元素数目
} else {
// 存在该节点,修改节点内容
DouLink* temp = it->second;
temp->value = value;
moveHead(temp);
}
}
// 封装的操作链表函数,添加到链表头部
void addHead(DouLink* node) {
node->next = head->next;
head->next->prev = node;
head->next = node;
node->prev = head;
}
// 封装的操作链表函数,移动到链表头部
void moveHead(DouLink* node) {
remove(node); // 删除节点
addHead(node); // 将节点添加到头部
}
// 删除某节点
void remove(DouLink* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
// 删除链表尾部节点,长时间未被访问节点
DouLink* removeTail() {
DouLink* temp = tail->prev;
remove(temp);
return temp;
}
};
码字不易,欢迎大家点赞,关注,评论,谢谢!
C++训练营
专为校招、社招3年工作经验的同学打造的1V1 C++训练营,量身定制学习计划、每日代码review,简历优化,面试辅导,已帮助多名学员获得大厂offer!