既然上一篇写了LRU,那这一篇我们就来搞一下LFU吧。
LFU,Least Frequently Used,最近最少使用,其核心思想是,“如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小”。本文基于 LeetCode 460. LFU 缓存 进行,解法不是最优,但是实现了Get
和Put
操作的时间复杂度均为O(1)。
这里我们使用一个HashMap实现Get操作的O(1)复杂度,同时使用另一个HashMap,其Key为频次,Value为该频次下缓存节点的双向链表,用于保存该频次的缓存节点,用于维护最小频次。具体算法详解可以看LeetCode官解的方法二(双哈希表),这里不多赘述。
直接上代码:
package lfu
type LFUCache struct {
cache map[int]*LFUCacheNode
freqMap map[int]*LFUCacheList
minFreq int // 最小频次
cap int
}
// 双向链表
type LFUCacheList struct {
head *LFUCacheNode // 虚拟头节点
end *LFUCacheNode // 虚拟尾节点
length int // 链表长度
}
func newLFUCacheList() *LFUCacheList {
list := &LFUCacheList{
head: newLFUCacheNode(0, 0),
end: newLFUCacheNode(0, 0),
}
list.head.next = list.end
list.end.pre = list.head
list.length = 2
return list
}
func (l *LFUCacheList) removeNode(node *LFUCacheNode) bool {
node.removeNode()
l.length--
// 查询长度,若小于等于2,即该链表没有元素,通知删除该链表
if l.length <= 2 {
return true
}
return false
}
// 将节点添加到双向链表前面
func (l *LFUCacheList) addToHead(node *LFUCacheNode) {
node.moveToHead(l.head)
l.length++
}
// 获取链表的最后一个节点
func (l *LFUCacheList) getLast() *LFUCacheNode {
return l.end.pre
}
// 链表节点
type LFUCacheNode struct {
key int // Key
value int // Value
pre *LFUCacheNode // 前驱结点
next *LFUCacheNode // 后驱节点
freq int // 频次
}
func newLFUCacheNode(key int, value int) *LFUCacheNode {
return &LFUCacheNode{
key: key,
value: value,
freq: 0,
}
}
// 移除节点
func (n *LFUCacheNode) removeNode() {
n.pre.next = n.next
n.next.pre = n.pre
n.pre = nil
n.next = nil
}
// 插入最前面
func (n *LFUCacheNode) moveToHead(head *LFUCacheNode) {
head.next.pre = n
n.next = head.next
n.pre = head
head.next = n
}
func Constructor(capacity int) LFUCache {
return LFUCache{
cache: make(map[int]*LFUCacheNode, capacity),
freqMap: make(map[int]*LFUCacheList),
cap: capacity,
minFreq: 0,
}
}
func (c *LFUCache) Get(key int) int {
// bad case
if c.cap < 1 {
return -1
}
// 查询节点
node, ok := c.cache[key]
// 不存在直接返回-1
if !ok {
return -1
}
// 更新频次链表
c.refreshNode(node)
return node.value
}
func (c *LFUCache) Put(key int, value int) {
// bad case
if c.cap < 1 {
return
}
// 查询节点
node, ok := c.cache[key]
// 节点存在,更新节点
if ok {
node.value = value
c.refreshNode(node)
return
}
// 创建节点
node = newLFUCacheNode(key, value)
// 当长度小于容量时,直接插入,并更新节点
if len(c.cache) < c.cap {
c.cache[key] = node
c.refreshNode(node)
return
}
// 长度大于等于容量,需要删除最不常使用的节点
list := c.freqMap[c.minFreq]
lastNode := list.getLast()
isDelete := list.removeNode(lastNode)
if isDelete {
delete(c.freqMap, c.minFreq)
}
delete(c.cache, lastNode.key)
// 插入节点
c.cache[key] = node
c.refreshNode(node)
return
}
// 更新节点
func (c *LFUCache) refreshNode(node *LFUCacheNode) {
// 获取原频次链表
list, ok := c.freqMap[node.freq]
// 原频次存在,说明是旧节点,需要删除,不存在则为新节点
if ok {
// 删除节点
isDeleteList := list.removeNode(node)
if isDeleteList {
delete(c.freqMap, node.freq)
// 当需要删除时,说明该频次已无节点,如果最小频次为该节点,则需更新最小频次
if c.minFreq >= node.freq {
c.minFreq = node.freq + 1
}
}
} else {
// 原频次不存在,说明是新节点,频次必定为最小,更新频次
c.minFreq = node.freq + 1
}
// 更新频次
node.freq += 1
// 查询新频次链表
newList, ok := c.freqMap[node.freq]
// 若存在,直接插入
if ok {
newList.addToHead(node)
return
}
// 若不存在,创建链表并插入
newList = newLFUCacheList()
c.freqMap[node.freq] = newList
newList.addToHead(node)
return
}
测试代码:
package lfu
import (
"fmt"
"testing"
)
func TestLC460(t *testing.T) {
lfu := Constructor(2)
lfu.Put(1, 1)
lfu.Put(2, 2)
fmt.Println(lfu.Get(1))
lfu.Put(3, 3)
fmt.Println(lfu.Get(2))
fmt.Println(lfu.Get(3))
lfu.Put(4, 4)
fmt.Println(lfu.Get(1))
fmt.Println(lfu.Get(3))
fmt.Println(lfu.Get(3))
}
执行结果:
(End)
8 条评论
真好呢
你的文章充满了欢乐,让人忍不住一笑。 http://www.55baobei.com/aEIMsyfBLh.html
如何高效地在私服传奇中运用道士技能打架?:https://501h.com/jingpin/2024-10-12/41813.html
你的文章让我感受到了艺术的魅力,谢谢! http://www.55baobei.com/UDOcjb9gmO.html
文章的确不错啊https://www.cscnn.com/
riqdps13082BP-适合各类读者,无论是年轻人还是老年人,都能找到自己感兴趣的内容。http://m.keroger.cn//
鬼哥吗?
确实