Leetcode460. LFU 缓存
Leetcode460. LFU 缓存
题目描述
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象int get(int key)
- 如果键存在于缓存中,则获取键的值,否则返回 -1。void put(int key, int value)
- 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get
和 put
函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
示例:
1 | 输入: |
提示:
- 最多调用 $10^5 $次
get
和put
方法
**进阶:**你可以为这两种操作设计时间复杂度为 O(1)
的实现吗?
解题思路
首先得定义一个缓存的结点结构:
1 | struct Node |
里面包含key
,value
和frenquency
信息。
如果要让get
操作达到的时间复杂度,缓存结点可以存储在动态数组或者哈希表中。但是如果用动态数组存储的话,put
操作肯定无法达到的时间复杂度。要使put
操作达到的时间复杂度,得用双向链表来组织缓存结点。所以,如果要让put
和get
操作同时达到的时间复杂度,我们可以考虑用哈希表+双向链表的方式来实现。
首先定义一个frequency_table
哈希表,索引为频率frenquency
,每一个索引存放一个双向链表,这个双向链表里面存放所有使用频率为frenquency
的缓存。每一次添加的时候在这个双向链表的头部进行插入,在删除的时候在双向链表的尾部删除,这样就可以实现访问时间的有序排列,用于实现LFU。然后我们还需要定义一个变量来记录最小的使用频率minfreq
,用于删除操作。
接着,我们还需要定义一个key_table
的哈希表,索引为key
,每一个索引存放这key
值对应的frequency_table
中链表的具体节点内存地址。然后利用这两个哈希表就可以让put
和get
操作同时达到的时间复杂度。
对于 get(key)
操作,我们能通过索引key
在 key_table
中找到缓存在 frequency_table
中的链表的结点的内存地址,如果不存在直接返回 -1,否则我们能获取到对应缓存的相关信息,这样我们就能知道缓存的键值还有使用频率,直接返回 key 对应的值即可。但是,进行一次get(key)
操作,这个缓存的使用频率就要加一,同时就需要更新这两个哈希表的值。还有可能需要更新minfreq
的值。在整体的操作中,都是常数时间的复杂度,所以时间复杂度为。
对于 put(key, value)
操作,我们先通过索引 key
在 key_table
中查看是否有对应的缓存,如果有的话,其实操作s类似于 get(key)
操作,唯一的区别就是我们需要将当前的缓存里的值更新为 value
。如果没有的话,相当于是新加入的缓存,如果缓存已经到达容量,需要先删除最近最少使用的缓存,再进行插入。
先考虑插入,由于是新插入的,所以缓存的使用频率一定是 1
,所以我们将缓存的信息插入到 frequency_table
中 1
索引下的列表头即可,同时更新key_table[key]
的信息,以及更新 minfreq = 1
。
那么剩下的就是删除操作了,由于我们实时维护了 minfreq
,所以我们能够知道 frequency_table
里目前最少使用频率的索引,同时因为我们保证了链表中从链表头到链表尾的插入时间是有序的,所以frequency_table[minfreq]
的链表中链表尾的节点即为使用频率最小且插入时间最早的节点,我们删除它同时根据情况更新 minfreq
,整个时间复杂度均为 。
具体细节如下述代码所述:
1 | struct Node |
时间复杂度:put
和get
操作都为
空间复杂度: