LFU 起夜级李姐

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分成两部分

  1. 高16位用来记录访问时间(单位为分钟)
  2. 低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的类似。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值