概述
LFU(Least Frequently Used)缓存是另一种TLB中的数据调度策略。它的思想是当缓存中没有多余空间时,踢出缓存中使用次数最少的数据。
例题
LeetCode 460. LFU 缓存
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象。int get(int key)
- 如果键key
存在于缓存中,则获取键的值,否则返回-1
。void put(int key, int value)
- 如果键key
已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量capacity
时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
1 | 输入: |
题解
哈希表+双向链表
与LRU缓存相同,我们使用哈希表来将键值映射为对应节点。LRU那题我蠢蠢地把 head
和 end
都标记为链表中的有效节点,这样处理起来会很麻烦。今天这题我选择使用“哨兵节点”,即在链表之外设置两个独立的、不储存任何数据的节点作为 head
和 end
,并将更新操作独立成函数,方便后续调用。
1 | class LFUCache { |
值得注意的一点是,由于同样调用次数的节点,我们要踢出最早调用过的(即 cnt
相同的若干个节点内部采用LRU算法),所以我们在更新的时候要将操作过的节点放到 cnt+1
的若干个节点中最靠后的位置,而找到这个位置当然是使用遍历的方法。所以喜闻乐见地超时了(最差的情况下时间复杂度到达平方级别)。
哈希表+二维双向链表
为了解决超时问题,我们可以借助桶排序的思想,把链表扩充为二维链表。其中一个链表使用链表储存某 cnt
值的所有节点,并按最早调用时间排序,即链表套链表。在更新时,我们只需取出调用过的节点,将这个节点取出放到包括了所有 cnt+1
的节点的那个链表的最后一位就可以了。
1 | class LFUCache { |
有的没的
为什么是冬眠舱
“冬眠”是《三体》中的设定。人可以通过“冬眠”来跨越漫长的时间。
重逢的欢喜中,他们交换了自己的经历。罗辑得知史强是两个月前苏醒的,他的白血病已经治好了,医生还发现他的肝脏病变的几率很高,可能是喝酒的原因,也顺便处理好了。其实,在他们的感觉中,两人分别到时间并不是太长,就是四五年的样子,冬眠中是没有时间感的,但在两个世纪后的新时代相遇,还是多了一层亲切感。——《三体 II · 黑暗森林》
我现在正处于一个人生的岔路口,无论是向左走还是向右走都会后悔,而且都是无法接受的后悔。所以我选择逃避,把自己的灵魂封印起来,以此度过漫长的煎熬时光,也称之为“冬眠”。我讨厌所谓的“顺其自然”,但遗憾的是我现在只能顺其自然。也许我永远没法做出选择,也许选择的权利根本不在我手上,也许未来以上帝视角回忆这段时光的我也许会觉得很可笑,但现在的我能做的也只有继续冬眠而已。