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 }
|