LFU -- Javascript实现版本
Posted 奋飛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LFU -- Javascript实现版本相关的知识,希望对你有一定的参考价值。
LFU(Least Frequently Used)
LFU 最近最不常用,是一种常见的淘汰(置换)算法,选择最近使用次数最少的予以淘汰。常用于内存管理。
对每个块进行引用计数,当缓存达到容量上限并有新的模块插入时,系统将搜索具有最低计数的块删除。
算法实现:双向链表 + 哈希表
-
节点:
Node {key, value, freq, pre, post}
key、value
freq
:使用频率, 删除时使用pre、post
: 前置及后置节点,插入使用
-
双向链表:
DualLinkedList {head, tail, remove(), add(), isEmpty()}
按照被使用的顺序存储 node,头部
head
为最近使用的,尾部tail
是最久未使用的。head、tail
:头及尾节点remove()
:移除节点add()
:添加节点(追加到 head 节点之后)isEmpty()
:链表是否为空
-
定义 Hash 表:
freqMap
:以频率(freq
)为索引,每个索引对应存放一个双向链表,链表中存放所有改频率的节点(key,value,freq
)cacheMap
: 以key
为索引,每个索引对应存放相应的 node 节点
/* 定义节点 */
class Node {
constructor (key, value) {
this.key = key
this.value = value
this.freq = 1 // 访问频次
this.pre = null
this.post = null
}
}
/* 双向链 表*/
class DualLinkedList {
constructor (node) {
this.head = new Node()
this.tail = new Node()
this.head.post = this.tail
this.tail.pre = this.head
}
/* 移除 */
remove (node) {
node.pre.post = node.post
node.post.pre = node.pre
}
/* 插入节点(head之后) */
add (node) {
node.post = this.head.post
this.head.post.pre = node
node.pre = this.head
this.head.post = node
}
/* 判空 */
isEmpty () {
return this.head.post === this.tail && this.tail.pre === this.head
}
}
class LFUCache {
constructor (capacity) {
this.capacity = capacity
this.minFreq = 0 // 最小使用频率,删除使用
this.size = 0 // 当前已使用的容量
this.freqMap = new Map() // freq-DualLinkedList
this.cacheMap = new Map() // key-node
}
// 频率+1,freqMap的旧频率的key中移除,添加到新的freq链表中
increaseFreq (node) {
let list = this.freqMap.get(node.freq)
list.remove(node)
// 同步更新最新频率标记(如果当前是最小频率节点)
if (list.isEmpty() && node.freq === this.minFreq) {
this.minFreq += 1
}
node.freq += 1
let newList = this.freqMap.get(node.freq)
if (newList === undefined) {
newList = new DualLinkedList()
}
newList.add(node) // 最新频率索引对应的双向链表中增加该节点
this.freqMap.set(node.freq, newList)
}
/* 获取节点 value */
get(key) {
let node = this.cacheMap.get(key)
if (node === undefined) {
// 不存在,返回 -1
return -1
}
// 存在,返回当前节点值;同时访问频次 +1
this.increaseFreq(node)
return node.value
}
/* 增加节点 */
put (key, value) {
if (this.capacity === 0) return
// 如果key已存在,则变更其值,增加访问频率
if (this.cacheMap.has(key)) {
let node = this.cacheMap.get(key)
node.value = value
this.cacheMap.set(key, node)
this.increaseFreq(node)
return
}
// 容量达到上限
if (this.size === this.capacity) {
// 获取最小频率队列(通过minFreq标记)
let miniFreqList = this.freqMap.get(this.minFreq)
// 获取最小频次队列的队尾(最小且最久)
let miniFreqNode = miniFreqList.tail.pre
// 淘汰该节点
miniFreqList.remove(miniFreqNode)
this.freqMap.set(this.minFreq, miniFreqList)
this.cacheMap.delete(miniFreqNode.key)
}
// 插入新节点
let node = new Node(key, value)
this.cacheMap.set(key, node)
// freq为1的链表中,插入节点
let list = this.freqMap.get(1)
if (list === undefined) {
list = new DualLinkedList()
}
list.add(node)
this.freqMap.set(1, list)
this.minFreq = 1
if (this.size < this.capacity) {
this.size += 1
}
return
}
}
复杂度:
- 时间复杂度:对于
put
和get
都是 O ( 1 ) O(1) O(1)。 - 空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity),因为哈希表和双向链表最多存储 $ capacity+1 $ 个元素。
借助 map 的 LRU 特性
map.delete(key)
再 map.set(key, value)
则 map 对应的key被移到了尾部。
1、Let M be the this value.
…
7、Append p as the last element of entries.
8、Return M.
/**
* 定义节点
*/
class Node {
constructor (key, value) {
this.key = key
this.value = value
this.freq = 1 // 访问频次
}
}
class LFUCache {
constructor (capacity) {
this.capacity = capacity
this.minFreq = 0 // 最小使用频率,删除使用
this.size = 0 // 当前已使用的容量
this.freqMap = new Map() // 频率-map<node>(不使用双向链表)
this.cacheMap = new Map() // key-node(用于get方法,快捷获取相对应key的value值)
}
/**
* 频率+1
* ① 从freqMap的旧频率的key中移除
* ② 旧频率为空 && 当前频率是最小频率,需要将最小频率+1
* ③ 添加到新频率(旧频率+1)对应的map队列中 => 当前队列没有需要新建
*/
increaseFreq (node) {
let map = this.freqMap.get(node.freq)
map.delete(node.key)
if (!map.size && node.freq === this.minFreq) {
this.minFreq += 1
}
node.freq += 1
let newMap = this.freqMap.get(node.freq)
if (newMap === undefined) {
newMap = new Map()
}
newMap.set(node.key, node)
this.freqMap.set(node.freq, newMap)
}
// 借助缓存的 cacheMap 处理
get(key) {
let node = this.cacheMap.get(key)
if (node === undefined) {
return -1
}
this.increaseFreq(node)
return node.value
}
put (key, value) {
if (this.capacity === 0) return
// 如果节点已存在,则变更其值
if (this.cacheMap.has(key)) {
let node = this.cacheMap.get(key)
node.value = value
this.cacheMap.set(key, node)
this.increaseFreq(node)
return
}
// 达到最大容量,需要删除访问频次最小&&最久的节点
// 访问频次最小=>通过minFreq标记获取相应的map
// map中第一个元素为最久未被访问元素(map lru特性决定)
if (this.size === this.capacity) {
let miniFreqMap = this.freqMap.get(this.minFreq)
let miniFreqNode = miniFreqMap.get(miniFreqMap.keys().next().value) // 频次最小,第一个key(借助map的lru特性)
miniFreqMap.delete(miniFreqNode.key)
this.freqMap.set(this.minFreq, miniFreqMap)
this.cacheMap.delete(miniFreqNode.key)
}
// 增加节点元素(这里只处理节点不存在,已存在上面已处理)
let node = new Node(key, value)
this.cacheMap.set(key, node)
let map = this.freqMap.get(1)
if (map === undefined) {
map = new Map()
}
map.set(node.key, node)
// 对应新节点,访问频次一定是1,且最小频次为1
this.freqMap.set(1, map)
this.minFreq = 1
if (this.size < this.capacity) {
this.size += 1
}
return
}
}
复杂度:
- 时间复杂度:对于
put
和get
都是 O ( 1 ) O(1) O(1) - 空间复杂度: O ( c a p a c i t y ) O(capacity) O(capacity)
以上是关于LFU -- Javascript实现版本的主要内容,如果未能解决你的问题,请参考以下文章
详解三种缓存过期策略LFU,FIFO,LRU(附带实现代码)