Go代码实现
type Node struct {
key, val, freq int
pre, next *Node
}
type DoubleLink struct {
head, tail *Node
length int
}
type LFUCache struct {
freq_map map[int]*DoubleLink
key_map map[int]*Node
cap int
}
func Constructor(capacity int) LFUCache {
return LFUCache{
freq_map: map[int]*DoubleLink{},
key_map: map[int]*Node{},
cap: capacity,
}
}
// 初始化双向链表
func InitDoubleLink() *DoubleLink {
list := &DoubleLink{
head: &Node{},
tail: &Node{},
length: 0,
}
list.head.next = list.tail
list.tail.pre = list.head
return list
}
// 插入双向链表头部
func InsertHead(list *DoubleLink, node *Node) {
node.next = list.head.next
list.head.next = node
node.next.pre = node
node.pre = list.head
list.length++
}
// 删除 尾部节点
func RemoveTail(list *DoubleLink) (key int) {
node := list.tail.pre
list.tail.pre = node.pre
node.pre.next = list.tail
list.length--
return node.key
}
// 删除节点
func RemoveNode(list *DoubleLink, node *Node) {
node.pre.next = node.next
node.next.pre = node.pre
list.length--
}
func (this *LFUCache) Get(key int) int {
node, ok := this.key_map[key]
if !ok {
return -1
}
list := this.freq_map[node.freq]
RemoveNode(list, node)
// 频率 +1
node.freq += 1
// 把这个 节点 装入 新频率 的 链表头部
list2, ok := this.freq_map[node.freq]
if !ok {
list2 = InitDoubleLink()
this.freq_map[node.freq] = list2
}
InsertHead(list2, node)
return node.val
}
func (this *LFUCache) Put(key int, value int) {
if this.cap == 0 {
return
}
node, ok := this.key_map[key]
// 覆盖旧的value
if ok {
// 原来的频率 +1
list := this.freq_map[node.freq]
RemoveNode(list, node)
node.freq++
node.val = value
// 把这个 节点 装入 新频率 的 链表头部
list2, ok := this.freq_map[node.freq]
if !ok {
list2 = InitDoubleLink()
this.freq_map[node.freq] = list2
}
InsertHead(list2, node)
} else {
// 容量判断 触发淘汰
if this.cap < len(this.key_map)+1 {
// 从频率 低到高 的找,看是否存在双向链表
for i := 1; ; i++ {
list, ok := this.freq_map[i]
if ok && list.length > 0 {
kk := RemoveTail(list)
delete(this.key_map, kk)
break
}
}
}
// 装入keymap
node = &Node{
key: key,
val: value,
freq: 1, // 新增的频率都是1
}
this.key_map[key] = node
// 装入 freq_map 频率=1 的 链表头部
list, ok := this.freq_map[1]
if !ok {
list = InitDoubleLink()
this.freq_map[1] = list
}
InsertHead(list, node)
}
}
面试相关
对比前文,会发现,LFU的实现远远比LRU的复杂,如果只是写伪代码的话,或许还好些,但是要写出能够通过leetcode提交的代码,大家也看到了,并不容易。所以我认为,对于LFU,知道这个算法的逻辑,以及如何优化成O(1)就OK了,没有必要纯手写。
Redis中的LFU
没有意外,redis中的lfu实现依然不是上述代码所展现的那种方式。
问题1:上述代码是实打实的记录了使用频率,这很占用内存(站在redis的视角)
问题2:可能一段时间某个key使用频率超高,但后续再不使用,这样这些按理说应该淘汰的key因为频率太高,一直淘汰不了。
Redis中的LFU算法利用的依然是redisObject中的lru字段(24bit)
前情回顾:lru这个字段在Redis的LRU策略中,用作记录时间戳的。
配置
lfu-log-factor 10
lfu-decay-time 1
lfu-log-factor
可以调整计数器counter(后8位)的增长速度,lfu-log-factor
越大,counter
增长的越慢。
lfu-decay-time
是一个以分钟为单位的数值,可以调整counter
的减少速度
把lru字段的24bit分成两部分
- 高16位用来记录访问时间(单位为分钟)
- 低8位用来记录访问频率,简称counter(8位只能代表255)
counter并不是一个简单的线性计数器,而是用基于概率的对数计数器来实现。
别管怎么算的,就记结论:默认server.lfu_log_factor为10的情况下,8 bits的counter可以表示大约1百万的访问频率。
这就把问题1解决了。
为了解决问题2,redis提供了衰减因子server.lfu_decay_time,其单位为分钟,
如果一个key长时间没有访问,那么它的计数器counter就要减少,减少的值由衰减因子来控制。
默认为1的情况下也就是N分钟内没有访问,counter就要减N。
LFU信息的采样方式与LRU的类似。