460. LFU 缓存

1. 题号和题目名称

  1. LFU 缓存

2. 题目叙述

请你为最不经常使用(LFU)缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity):用数据结构的容量 capacity 初始化对象。
  • int get(int key):如果键 key 存在于缓存中,则获取键的值,否则返回 -1。
  • void put(int key, int value):如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。

注意「项的使用次数」就是自插入该项以来对其调用 getput 函数的次数之和。使用次数会在对应项被移除后置为 0 。

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 getput 操作,使用计数器的值将会递增。

示例

输入
["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1);      // 返回 1
                 // cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                 // cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                 // cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4);      // 返回 4
                 // cache=[4,3], cnt(4)=2, cnt(3)=3

3. 模式识别

本题需要实现一个 LFU 缓存,关键在于维护键值对、每个键的使用频率以及每个频率对应的键值对链表。可以使用哈希表来快速查找键值对和频率对应的链表,使用双向链表来维护每个频率下键值对的访问顺序,以便在频率相同的情况下移除最近最久未使用的键。

4. 考点分析

  • 哈希表的使用:用于快速查找键值对和频率对应的链表。
  • 双向链表的操作:维护每个频率下键值对的访问顺序。
  • 缓存淘汰策略:实现 LFU 缓存的淘汰逻辑,在容量满时移除最不经常使用且最近最久未使用的键。

5. 所有解法

  • 哈希表 + 双向链表解法:使用哈希表存储键值对和频率对应的链表,双向链表维护每个频率下键值对的访问顺序,是本题的最优解法。

6. 最优解法(哈希表 + 双向链表解法)的 C 语言代码

/*
数值链表的节点定义。
*/
typedef struct ValueListNode_s
{
   
    int key;          // 键,用于标识数值节点对应的键值对中的键
    int value;        // 值,用于存储键值对中的值
    int counter;      // 计数,记录该节点被访问的次数(即使用频率)
    struct ValueListNode_s *prev;  // 指向前一个节点的指针,用于双向链表的前驱连接
    struct ValueListNode_s *next;  // 指向后一个节点的指针,用于双向链表的后继连接
}
ValueListNode;

/*
计数链表的节点定义。
其中,head是数值链表的头节点,对应的是最新的数值节点。
环形链表,head->prev实际就是tail,对应的就是最久未使用的节点。
*/
typedef struct CounterListNode_s
{
   
    ValueListNode *head;  // 指向数值链表的头节点,用于管理具有相同使用频率的数值节点
    struct CounterListNode_s *prev;  // 指向前一个计数链表节点的指针,用于双向链表的前驱连接
    struct CounterListNode_s *next;  // 指向后一个计数链表节点的指针,用于双向链表的后继连接
}
CounterListNode;

/*
对象结构定义。
capacity:           总的容量,即缓存能够容纳的键值对的最大数量。
currentCounter:     当前已有的key的数量,记录缓存中实际存储的键值对的个数。
keyHash:            key的哈希数组,用于快速查找键对应的数值节点,为空表示这个key对应数值不存在。
counterHash:        counter的哈希数组,用于快速查找使用频率对应的计数链表节点,为空表示这个counter对应的链表不存在。
head:               计数链表的头节点,指向计数最小(即使用频率最低)的计数链表节点。
*/
typedef struct
{
   
    int capacity;
    int currentCounter;
    ValueListNode **keyHash;
    CounterListNode **counterHash;
    CounterListNode *head;
}
LFUCache;

/*
几个自定义函数的声明,具体实现见下。
*/
extern void removeValueNode(CounterListNode *counterNode, ValueListNode *valueNode);
extern void insertValueNode(CounterListNode *counterNode, ValueListNode *valueNode);
extern void removeCounterNode(LFUCache *obj, CounterListNode *counterNode);
extern void insertCounterNode(LFUCache *obj, CounterListNode *counterPrev, CounterListNode *counterNode);

/*
创建对象。
*/
LFUCache *lFUCacheCreate(int capacity)
{
   
    LFUCache *obj = (LFUCache *)malloc(sizeof(LFUCache));
    /* 总容量就等于入参capacity,当前已有的key的数量初始化为0。 */
    obj->capacity = capacity;
    obj->currentCounter = 0;
    /* key的取值范围是[0, 10^5],共100001个。用calloc代替malloc,即包含了初始化为空的步骤。 */
    obj->keyHash = (ValueListNode **)calloc(100001, sizeof(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

请向我看齐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值