LFUCache
LFU是Least Frequently Used的缩写,即最不经常最少使用算法,也是一种常用的页面置换算法,选择访问计数器最小的页面,并予以淘汰。
根据LFU的策略,我们每次访问对象都需要更新访问计数器,当插入B的时候,发现缓存中有B,所以增加访问计数器,兵把B移动到访问计数器从大到小排序的地方。当插入一个元素时,先更新访问计数器,在移动到它排序以后的位置,如果页面中已经满了,就将末尾的元素进行剔除
当插入一个数据时,尾部有多个数据计数器值相同,这个时候插入是从相同计数器顶端开始插入,这样就能保证每次剔除的都是计数器值相同的比较久的页面,按照这种数据插入方式,计数器尾部的数值可以保证是比较旧的数据,这一点和LRU有点不同
LFU更新和插入新页面可以发生在链表中的任意位置,删除页面都发生在表尾
LFU同样需要尽量的高效,O(1)内查询,修改删除也要O(1)内完成。
type LFUCache struct {
nodes map[int]*list.Element
lists map[int]*list.List
capacity int
min int
}
type node struct {
key int
value int
frequency int
}
为了实现O(1)的时间复杂度,依然需要采用map和双向链表的实现方式。但是相比LRU这里需要额外存储一个访问次数。
还有一个需要考虑的问题就是按照什么排序,相同顺序按照先后顺序?如果每次数据都需要排序,那么最少的时间复杂度也是O(nlogn)。仔细看LFU的策略,发现我们只需要保证最小频次的元素的按照先后顺序排序的就可以了,因此我们可以使用一个Min记录最少的频次,淘汰时从尾部找到这个最小频次的值,将其进行删除就行了
type LFUCache struct {
nodes map[int]*list.Element //
lists map[int]*list.List // 不同频次的数据按照不同的链表进行存储
capacity int
min int
}
type node struct {
key int
value int
frequency int
}
func Constructor(capacity int) LFUCache {
return LFUCache{
nodes: make(map[int]*list.Element),
lists: make(map[int]*list.List),
capacity: capacity,
min: 0,
}
}
LFUCache的Get操作涉及更新Frequency值和两个map,在nodes map中通过Key获取节点信息,lists中的索引存储的是频次。当结点存在时,需要将lists中的节点删除,删除完之后对frequency进行++.如果新的frequency在Lists中存在,添加到双向链表的表首,如果不存在就重新创建一个双链表并把当前结点添加到表首。在更新双链表结点作为value的map,最后更新min的值,判断老的frequency对应的双链表中是否已经为空,如果空了min++
func (lfuCache *LFUCache) Get(key int) int {
value, ok := lfuCache.nodes[key]
if !ok {
return -1
}
currentNode := value.Value.(*node)
lfuCache.lists[currentNode.frequency].Remove(value)
currentNode.frequency++
if _, ok := lfuCache.lists[currentNode.frequency]; !ok {
lfuCache.lists[currentNode.frequency] = list.New()
}
// 按照频次取出链表
newList := lfuCache.lists[currentNode.frequency]
// 将当前的节点插入到对应链表里面
newNode := newList.PushFront(currentNode)
lfuCache.nodes[key] = newNode
if currentNode.frequency-1 == lfuCache.min && lfuCache.lists[currentNode.frequency-1].Len() == 0 {
lfuCache.min++
}
return currentNode.value
}
LFU的put操作逻辑稍微复杂一点,现在nodes map节点中查询key值是否存在,如果存在获取这个节点,更新它的value值,然后手动调用一次Get操作,以内下面的更新逻辑和Get操作一致。如果map中不存在就执行插入操作,并在插入之后判断caplity是否装满,如果装满执行删除操作,在min对应的链表中删除尾结点,对应的也要删除nodes map中的键值。
func (lfuCache *LFUCache) Put(key int, value int) {
if lfuCache.capacity == 0 {
return
}
// 如果存在,更新访问次数
if currentValue, ok := lfuCache.nodes[key]; ok {
currentNode := currentValue.Value.(*node)
currentNode.value = value
lfuCache.Get(key)
return
}
// 如果不存在,并且缓存已经满了
if lfuCache.capacity == len(lfuCache.nodes) {
currentList := lfuCache.lists[lfuCache.min]
backNode := currentList.Back()
delete(lfuCache.nodes, backNode.Value.(*node).key)
currentList.Remove(backNode)
}
// 新插入元素
lfuCache.min = 1
currentNode := &node{
key: key,
value: value,
frequency: 1,
}
if _, ok := lfuCache.lists[1]; !ok {
lfuCache.lists[1] = list.New()
}
newList := lfuCache.lists[1]
newNode := newList.PushFront(currentNode)
lfuCache.nodes[key] = newNode
}
代码仓库: https://github.com/zzu-andrew/algorithm
关注微信公众号:码上有话
一起来学习常用算法