手撕LFU

博主介绍:程序喵大人

最近是校招实习面试高峰期,训练营中很多同学都在面试中,有些同学甚至被大厂要求手撕LFU,觉得很离谱。

但其实手撕LFU在面试中已经不少见了,手撕LFU、手撕LRU,在现在的面试中都很常见,大家一定要掌握,平时可以多练几遍。

对应的力扣链接如下:

  • https://leetcode.cn/problems/lfu-cache/
  • https://leetcode.cn/problems/lru-cache/

相关LRU题解如下:

下面通过一个例子,来给大家说一下 LRU 的概念。

假设你是一位图书管理员,你需要管理一个热门书籍借阅专区,空间有限,只能存放一定数量的书籍。读者借阅书籍后需归还,当专区满了,又有新的热门书籍要上架时,你会怎么做呢?

你大概会去查看借阅记录,看哪些书籍被频繁借阅,频繁被借阅(相当于被访问)的书籍会继续留在专区,而那些很久都没有读者借阅(长时间未被访问 )的书籍,会将其从专区移除,放到普通书架,把空间让给新的热门书籍。

上面这个例子,就是我们常说的 LRU 缓存淘汰算法。

既然知道了 LRU 原理,下面我们来看一下题目要求

那我们来拆解一下,需要做哪些工作

  1. 设计一个数据结构,用来存储元素
  2. 维护数据结构里面的元素序列,能够做到需要腾出空间时,可以快速逐出最久未使用的关键字
  3. 能够快速的通过 key 获取 value,也就是做到随机访问

需求已经很明确了,那我们此时应该选择什么数据结构呢?因为需要快速获取 value,并且要 put key 和 value,那么数据结构肯定要有 HashMap。

在 LRU 算法里,我们要维护元素的访问顺序,每次访问一个节点,无论是新节点还是已有节点,都要把它移到有序序列的头部,以此表示它是最新被访问的。

这个有序序列会始终保持从头部到尾部,节点未被访问的时间依次递增。也就是说,序列头部的节点是刚刚被访问过的,而尾部的节点则是最久未被访问的,在需要淘汰元素时,就优先淘汰尾部节点。

以上所述,我们可以使用 **哈希表+链表 **来完成我们的需求。

那应该使用单向链表,还是双向链表呢?

移动节点到链表头部或删除链表尾部节点,都需先删除目标节点。

寻找后继节点时,单双向链表均可通过next指针在 O (1) 时间完成;但寻找前驱节点,单向链表需从头遍历,耗时 O (n),双向链表则能借前向指针在 O (1) 时间找到。因此,为保证操作均在 O (1) 时间内完成,故应选择双向链表,本质是以空间换时间。

好了,我们来看一下,具体是如何存储元素的呢?

从上图可知,我们的 key 为 int,value 为双向链表的节点

好啦,现在我们已经清晰知道了,应该如何设计数据结构,我们进一步根据题目,来了解需求

  1. LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存

解析: HashMap 需要有 size,并初始化 map 的 key 为 int,value 为双向链表的节点

  1. int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1

解析: 如果不存在返回 -1,如果存在,则将该 value 返回,并且将该节点,移到双向链表的头部,移到链表头部的操作我们可以分两步进行

第一步:将节点从当前位置删除

第二步:在链表头部 add 该节点

具体操作如下

这样就实现了,将某节点,移动到头部的操作

  1. 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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序喵大人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值