LFU算法介绍
LFU缓存机制
LFU 算法的淘汰策略是 Least Frequently Used,也就是每次淘汰那些使用次数最少的数据。对比于在LRU算法中介绍的LRU 缓存淘汰算法,LRU 算法的淘汰策略是 Least Recently Used,也就是每次淘汰那些最久没被使用的数据。LRU 算法的核心数据结构是使用哈希链表 LinkedHashMap
,首先借助链表的有序性使得链表元素维持插入顺序,同时借助哈希映射的快速访问能力使得我们可以在 O(1) 时间访问链表的任意元素。
而 LFU 算法相当于是把数据按照访问频次进行排序,这个需求恐怕没有那么简单,而且还有一种情况,如果多个数据拥有相同的访问频次,我们就得删除最早插入的那个数据。也就是说 LFU 算法是淘汰访问频次最低的数据,如果访问频次最低的数据有多条,需要淘汰最旧的数据。
算法描述
要求你写一个类,接受一个 capacity
参数,实现 get
和 put
方法:
1 2 3 4 5 6 7
| type LFUCache struct {}
func Constructor(capacity int) LFUCache {}
func (this *LFUCache) Get(key int) int {}
func (this *LFUCache) Put(key int, val int) {}
|
Get(key)
方法会去缓存中查询键 key
,如果 key
存在,则返回 key
对应的 val
,否则返回 -1。
Put(key, value)
方法插入或修改缓存。如果 key
已存在,则将它对应的值改为 val
;如果 key
不存在,则插入键值对 (key, val)
。
当缓存达到容量 capacity
时,则应该在插入新的键值对之前,删除使用频次(后文用 freq
表示)最低的键值对。如果 freq
最低的键值对有多个,则删除其中最旧的那个。
思路分析
首先我们需要理解LFU算法执行过程的几个事实:
- 调用
Get(key int)
方法时,要返回key
对应的val
。
- 只要用到
Get
或Put
方法访问一次某个key
,该key
的freq
就要加一。
- 如果在容量满了的时候插入,则需要将
freq
最小的 key
删除,如果最小的 freq
对应多个 key
,则删除其中最旧的那一个。
现在我们来逐一解决:
- 调用
Get(key int)
方法时,要返回key
对应的val
。
使用一个 HashMap
存储 key
到 val
的映射,就可以快速计算 get(key)
。
1
| keyToVal := make(map[int]int)
|
- 只要用到
Get
或Put
方法访问一次某个key
,该key
的freq
就要加一。
使用一个 HashMap
存储 key
到 freq
的映射,就可以快速操作 key
对应的 freq
。
1
| keyToFreq := make(map[int]int)
|
- 如果在容量满了的时候插入,则需要将
freq
最小的 key
删除,如果最小的 freq
对应多个 key
,则删除其中最旧的那一个。
这个需求应该是 LFU 算法的核心,所以我们分开说。
- 首先,肯定是需要
freq
到 key
的映射,用来找到 freq
最小的 key
。
- 将
freq
最小的 key
删除,那你就得快速得到当前所有 key
最小的 freq
是多少。想要时间复杂度 O(1) 的话,肯定不能遍历一遍去找,那就用一个变量 minFreq
来记录当前最小的 freq
吧。
- 可能有多个
key
拥有相同的 freq
,所以 freq
对 key
是一对多的关系,即一个 freq
对应一个 key
的列表。
- 希望
freq
对应的 key
的列表是存在时序的,便于快速查找并删除最旧的 key
。
- 希望能够快速删除
key
列表中的任何一个 **key
**,因为如果频次为 freq
的某个 key
被访问,那么它的频次就会变成 freq+1
,就应该从 freq
对应的 key
列表中删除,加到 freq+1
对应的 key
的列表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| freqToKeys := make(map[int]*LinkedHashSet) minFreq := 0
type node struct { key int prev *node next *node }
type DoubleList struct { head *node tail *node size int }
func constructorDL() *DoubleList { dh := &node{ key : 0, } dt := &node { key : 0, } dh.next,dt.prev = dt,dh return &DoubleList{ head : dh, tail : dt, size : 0, } }
type LinkedHashSet struct { hashMap map[int]*node doubleList *DoubleList }
func constructorLH() *LinkedHashSet { m := make(map[int]*node) dl := constructorDL() return &LinkedHashSet{ hashMap : m, doubleList : dl, } }
|
LinkedHashSet
顾名思义,是链表和哈希集合的结合体。链表不能快速访问链表节点,但是插入元素具有时序;哈希集合中的元素无序,但是可以对元素进行快速的访问和删除。那么,它俩结合起来就兼具了哈希集合和链表的特性,既可以在 O(1) 时间内访问或删除其中的元素,又可以保持插入的时序。
综上,我们可以写出 LFU 算法的基本数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| type LFUCache struct { keyToVal map[int]int keyToFreq map[int]int freqToKeys map[int]*LinkedHashSet minFreq int cap int }
func Constructor(capacity) LFUCache { kv := make(map[int]int) kf := make(map[int]int) fk := make(map[int]*LinkedHashSet) return LFUCache{ keyToVal : kv, keyToFreq : kf, freqToKeys : fk, minFreq : 0, cap : capacity, } }
func (this *LFUCache) Get(key int) int {}
func (this *LFUCache) Put(key int, val int) {}
|
代码框架
实现Get(key int)
方法
逻辑很简单,返回 key
对应的 val
,然后增加 key
对应的 freq
:
1 2 3 4 5 6 7
| func (this *LFUCache) Get(key int) int { if _,ok := this.keyToVal[key]; !ok { return -1 } this.increaseFreq(key) return this.keyToVal[key] }
|
增加key
对应的freq
是LFU算法的核心,所以我们干脆抽象一个方法increaseFreq
,这样Get
方法就比较简洁。
实现Put(key int,val int)
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| func (this *LFUCache) Put(key int, value int) { if this.cap <= 0 { return } if _,ok := this.keyToVal[key]; ok { this.keyToVal[key] = value this.increaseFreq(key) return } if this.cap == len(this.keyToVal) { this.removeMinFreqKey() } this.keyToVal[key] = value this.keyToFreq[key] = 1 if _,ok := this.freqToKeys[1]; !ok { this.freqToKeys[1] = constructorLH() } this.addRecently(key,1) this.minFreq = 1 }
|
实现核心逻辑(中间抽象层)
实现removeMinFreqKey
方法
1 2 3 4 5 6 7 8 9 10 11
| func (this *LFUCache) removeMinFreqKey() { keyList := this.freqToKeys[this.minFreq].doubleList deleteKey := keyList.removeFirst() delete(this.freqToKeys[this.minFreq].hashMap,deleteKey.key) if len(this.freqToKeys[this.minFreq].hashMap) == 0 { delete(this.freqToKeys,this.minFreq) } delete(this.keyToVal,deleteKey.key) delete(this.keyToFreq,deleteKey.key) }
|
删除某个key
肯定是要同时映射三个表的,借助minFreq
参数可以从FK
表中找到freq
最小的keyList
,删除其表头元素即可,同时删除Fk
表中freq
最小的hashMap
对应的元素。然后根据要被淘汰的deleteKey
删除其余两个表中的映射关系。
这里有个细节问题,如果keyList
中只有一个元素,那么删除之后minFreq
对应的key
列表就为空了,也就是minFreq
变量需要更新。实际上没办法快速计算minFreq
,只能线性遍历FK
表或者KF
表来计算,这样肯定不能保证 O(1) 的时间复杂度。但是,其实这里没必要更新minFreq
变量,因为你想想removeMinFreqKey
这个函数是在什么时候调用?在put
方法中插入新key
时可能调用。而你回头看put
的代码,插入新key
时一定会把minFreq
更新成 1,所以说即便这里minFreq
变了,我们也不需要管它。
实现increaseFreq
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func (this *LFUCache) increaseFreq(key int) { freq := this.keyToFreq[key] this.keyToFreq[key] = freq+1 this.freqToKeys[freq].doubleList.remove(this.freqToKeys[freq].hashMap[key]) delete(this.freqToKeys[freq].hashMap,key) if len(this.freqToKeys[freq].hashMap) == 0 { delete(this.freqToKeys,freq) if freq == this.minFreq { this.minFreq++ } } if _,ok := this.freqToKeys[freq+1]; !ok { this.freqToKeys[freq+1] = constructorLH() } this.addRecently(key,freq+1) }
|
实现addRecently
方法
1 2 3 4 5 6 7
| func (this *LFUCache) addRecently(key int, freq int) { x := &node{ key : key, } this.freqToKeys[freq].doubleList.addLast(x) this.freqToKeys[freq].hashMap[key] = x }
|
在插入新的节点时,需要修改FK
表的哈希表和双链表。
对底层数据结构进行操作
该层的主要作用是对我们实现的LinkedHashSet
结构封装一些通用操作,用以满足FK
表的更新和删除操作,主要包含以下几个方法:
- 删除
DoubleList
中某一节点的方法remove(x *node)
。
- 删除
DoubleList
中表首元素的方法removeFirst()
。
- 在
DoubleList
中表尾部增加新元素的addLast()
。
实现remove
方法
1 2 3 4
| func (dl *DoubleList) remove(x *node) { x.prev.next = x.next x.next.prev = x.prev }
|
实现removeFirst
方法
1 2 3 4 5 6 7 8
| func (dl *DoubleList) removeFirst() *node { if dl.head.next == dl.tail { return nil } first := dl.head.next dl.remove(first) return first }
|
实现addLast
方法
1 2 3 4 5 6
| func (dl *DoubleList) addLast(x *node) { x.prev = dl.tail.prev x.next = dl.tail dl.tail.prev.next = x dl.tail.prev = x }
|
总体算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
| type node struct { key int prev *node next *node }
type DoubleList struct { head *node tail *node size int }
func constructorDL() *DoubleList { dh := &node{ key : 0, } dt := &node { key : 0, } dh.next,dt.prev = dt,dh return &DoubleList{ head : dh, tail : dt, size : 0, } }
type LinkedHashSet struct { hashMap map[int]*node doubleList *DoubleList }
func constructorLH() *LinkedHashSet { m := make(map[int]*node) dl := constructorDL() return &LinkedHashSet{ hashMap : m, doubleList : dl, } }
func (dl *DoubleList) remove(x *node) { x.prev.next = x.next x.next.prev = x.prev }
func (dl *DoubleList) removeFirst() *node { if dl.head.next == dl.tail { return nil } first := dl.head.next dl.remove(first) return first }
func (dl *DoubleList) addLast(x *node) { x.prev = dl.tail.prev x.next = dl.tail dl.tail.prev.next = x dl.tail.prev = x }
type LFUCache struct { keyToVal map[int]int keyToFreq map[int]int freqToKeys map[int]*LinkedHashSet minFreq int cap int }
func Constructor(capacity int) LFUCache { kv := make(map[int]int) kf := make(map[int]int) fk := make(map[int]*LinkedHashSet) return LFUCache{ keyToVal : kv, keyToFreq : kf, freqToKeys : fk, minFreq : 0, cap : capacity, } }
func (this *LFUCache) Get(key int) int { if _,ok := this.keyToVal[key]; !ok { return -1 } this.increaseFreq(key) return this.keyToVal[key] }
func (this *LFUCache) Put(key int, value int) { if this.cap <= 0 { return } if _,ok := this.keyToVal[key]; ok { this.keyToVal[key] = value this.increaseFreq(key) return } if this.cap <= len(this.keyToVal) { this.removeMinFreqKey() } this.keyToVal[key] = value this.keyToFreq[key] = 1 if _,ok := this.freqToKeys[1]; !ok { this.freqToKeys[1] = constructorLH() } this.addRecently(key,1) this.minFreq = 1 }
func (this *LFUCache) removeMinFreqKey() { keyList := this.freqToKeys[this.minFreq].doubleList deleteKey := keyList.removeFirst() delete(this.freqToKeys[this.minFreq].hashMap,deleteKey.key) if len(this.freqToKeys[this.minFreq].hashMap) == 0 { delete(this.freqToKeys,this.minFreq) } delete(this.keyToVal,deleteKey.key) delete(this.keyToFreq,deleteKey.key) }
func (this *LFUCache) increaseFreq(key int) { freq := this.keyToFreq[key] this.keyToFreq[key] = freq+1 this.freqToKeys[freq].doubleList.remove(this.freqToKeys[freq].hashMap[key]) delete(this.freqToKeys[freq].hashMap,key) if len(this.freqToKeys[freq].hashMap) == 0 { delete(this.freqToKeys,freq) if freq == this.minFreq { this.minFreq++ } } if _,ok := this.freqToKeys[freq+1]; !ok { this.freqToKeys[freq+1] = constructorLH() } this.addRecently(key,freq+1) }
func (this *LFUCache) addRecently(key int, freq int) { x := &node{ key : key, } this.freqToKeys[freq].doubleList.addLast(x) this.freqToKeys[freq].hashMap[key] = x }
|