1. 题号和题目名称
- LFU 缓存
2. 题目叙述
请你为最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
:用数据结构的容量capacity
初始化对象。int get(int key)
:如果键key
存在于缓存中,则获取键的值,否则返回 -1。void put(int key, int value)
:如果键key
已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量capacity
时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get
和 put
函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
示例:
输入
["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(