[TypeScript]手撸LFU
最近做笔试的时候遇到了要手撸LFU的题目,LFU在vue源码里还是有使用的,例如keep-alive的实现机制就是基于它来搞的。不多说了,直接上代码。
代码
// 双向链表node
class DoubleLinkNode {key: number;val: number;freq: number;prev?: DoubleLinkNode | null;next?: DoubleLinkNode | null;constructor(key, val, freq) {this.freq = freq;this.key = key;this.val = val;}
}// 双向链表
class DoubleLink {size: number;head: DoubleLinkNode | null;tail: DoubleLinkNode | null;constructor() {this.size = 0;this.head = null;this.tail = null;}// 删除一个节点remove(node: DoubleLinkNode) {// 空链表if (this.size < 1) {return;}// 只有一个元素if (this.size === 1) {this.size = 0;this.head = null;this.tail = null;return;}// 删的是头结点if (node === this.head) {const nextNode = node.next;nextNode.prev = null;this.head = nextNode;this.size--;return;}// 删的是尾结点if (node === this.tail) {const prevNode = node.prev;prevNode.next = null;this.tail = prevNode;this.size--;return;}// 正常删中间节点const prevNode = node.prev;const nextNode = node.next;prevNode.next = nextNode;nextNode.prev = prevNode;this.size--;}// 在尾部插入一个节点addLast(node: DoubleLinkNode) {// 空链表if (this.size === 0) {this.head = node;this.tail = node;} else {// 非空链表const curTail = this.tail;curTail.next = node;node.prev = curTail;this.tail = node;}this.size++;}// 是否是空链表isEmpty() {return this.size === 0;}
}// 实现LFU缓存
class LFUCache {keyToNodeMap: Map<number, DoubleLinkNode>;freqToKeysMap: Map<number, DoubleLink>;capacity: number;minFreq: number;constructor(capacity: number) {this.keyToNodeMap = new Map();this.freqToKeysMap = new Map();this.capacity = capacity;this.minFreq = 0;}get(key: number): number {if (!this.keyToNodeMap.has(key)) {return -1;}const node = this.keyToNodeMap.get(key);// 增加频次this.increaseFreq(node);return node.val;}put(key: number, value: number): void {// key已经存在if (this.keyToNodeMap.has(key)) {// 修改对应的node的val即可const node = this.keyToNodeMap.get(key);node.val = value;this.increaseFreq(node);} else {// key不存在// 容量满了if (this.keyToNodeMap.size >= this.capacity) {// 删除最小频次中最久没使用的this.removeMinFreqKey();}// ------------正式开始插入------------const newNode = new DoubleLinkNode(key, value, 1);this.keyToNodeMap.set(key, newNode);if (!this.freqToKeysMap.get(1)) {this.freqToKeysMap.set(1, new DoubleLink());}// 维护频次表const link = this.freqToKeysMap.get(1);link.addLast(newNode);// 插入新 key 后最小的 freq 肯定是 1this.minFreq = 1;}}// 增加频次increaseFreq(node: DoubleLinkNode) {const oldFreq = node.freq;const newFreq = node.freq + 1;node.freq = newFreq;// 维护频次表const oldFreqLink = this.freqToKeysMap.get(oldFreq);// 从旧频次表中删除这个nodeoldFreqLink.remove(node);if (oldFreqLink.isEmpty()) {this.freqToKeysMap.delete(oldFreq);// 如果这个频次正好是最低频次,记得更新最小频次if (this.minFreq === oldFreq) {this.minFreq = newFreq;}}// 维护新频次表if (!this.freqToKeysMap.get(newFreq)) {this.freqToKeysMap.set(newFreq, new DoubleLink());}const newFreqLink = this.freqToKeysMap.get(newFreq);newFreqLink.addLast(node);}// 删除最小频次中最久没使用的removeMinFreqKey() {const minFreqLink = this.freqToKeysMap.get(this.minFreq);// 其中最先被插入的那个 node 就是该被淘汰的 nodeconst deletedNode = minFreqLink.head;// 维护最小频次mapminFreqLink.remove(deletedNode);if (minFreqLink.isEmpty()) {this.freqToKeysMap.delete(this.minFreq);}// key表中删除对应Nodethis.keyToNodeMap.delete(deletedNode.key);}// log调试方法print() {console.log('keyToNodeMap: ', this.keyToNodeMap);console.log('freqToKeysMap: ', this.freqToKeysMap);}
}
思路
LFU(Least Frequently Used)缓存是一种缓存淘汰策略,它根据元素的使用频率来决定哪些元素应该被淘汰。在你提供的代码中,LFUCache
类实现了这种策略,下面是它的工作原理的详细描述:
-
数据结构:
DoubleLinkNode
:表示双向链表中的节点,包含键(key)、值(val)和频率(freq)。DoubleLink
:表示双向链表,包含头尾节点以及链表的大小。Map
:keyToNodeMap
用于存储键到节点的映射,freqToKeysMap
用于存储频率到键的映射。
-
构造函数:
- 初始化
LFUCache
时,设置缓存的容量(capacity
),并初始化键到节点的映射和频率到键的映射。
- 初始化
-
get 方法:
- 检查键是否存在于
keyToNodeMap
中。 - 如果存在,找到对应的节点,并调用
increaseFreq
方法来增加节点的使用频率。 - 返回节点的值。
- 检查键是否存在于
-
put 方法:
- 检查键是否已存在:
- 如果存在,更新节点的值,并增加频率。
- 如果不存在,并且缓存已满,则调用
removeMinFreqKey
方法来删除最小频率的键。
- 创建新节点,并将其添加到
keyToNodeMap
和freqToKeysMap
中。
- 检查键是否已存在:
-
increaseFreq 方法:
- 增加节点的使用频率。
- 从旧频率的链表中删除节点,并检查是否需要更新最小频率。
- 将节点添加到新频率的链表中。
-
removeMinFreqKey 方法:
- 找到最小频率链表的头节点,即最久未使用的节点。
- 从链表中删除该节点,并更新
freqToKeysMap
。 - 从
keyToNodeMap
中删除对应的键。
-
print 方法:
- 用于调试,打印当前的键到节点映射和频率到键的映射。
这种实现方式确保了:
- 每个键的使用频率都被跟踪。
- 当缓存达到容量限制时,最少使用频率的键将被淘汰。
- 通过双向链表,可以快速地添加和删除节点,同时保持链表的顺序。
这种缓存策略适用于那些需要平衡访问频率和最近使用情况的场景。