引言
今日的算法打卡题是一道困难题,毫无悬念,没有做出来,但是还是记录一下自己的思考过程,以及需要去学习和掌握的知识点
题目描述
过程
分析
在看完题目描述之后,我接着便是好好研究了一下它提供的示例:
看完之后,有了大概的思路:
- 需要两个对象,
LFUCacheItem对象表示每个缓存项;LFUCache对象则是用来维护所有缓存项的容器 LFUCacheItem对象包括的属性:- key:键
- value:值
- usageCount:缓存项使用次数计数器
- lastAccessed: 最后一次访问缓存项的时间
LFUCache对象包括的属性:- capacity:缓存的最大容量
- cache:一个字典,用于将键映射到对应缓存项(LFUCacheItem对象)
LFUCache对象包括的方法:- get:获取缓存中存在的缓存项的值;缓存项存在返回值,不存在返回 -1
- put:向缓存中加入缓存项;缓存项存在更新值,不存在创建新值加入
- 超出缓存容量需要
evict最不常使用的缓存项
- 超出缓存容量需要
- evict:当缓存满了之后,删除最不常使用的缓存项
- 判断标准:usageCount + lastAccessed
- 当
usageCount相同的情况下,根据lastAccessed决定使用频率
应用
根据上面对题目的剖析之后,实现的代码如下,一比一复刻,就是加了一些对于边界条件的判断:
var LFUCache = function(capacity) {
this.capacity = capacity;
this.cache = new Map();
};
var LFUCacheItem = function(key, value, usageCount = 1) {
this.key = key;
this.value = value;
this.usageCount = usageCount;
this.lastAccessed = performance.now();
}
LFUCache.prototype.get = function(key) {
if(this.cache.has(key)) {
const cacheItem = this.cache.get(key);
cacheItem.usageCount += 1;
cacheItem.lastAccessed = performance.now();
return cacheItem.value;
}
return -1;
};
LFUCache.prototype.put = function(key, value) {
if(this.capacity === 0) return;
if(this.cache.has(key)) {
const cacheItem = this.cache.get(key);
cacheItem.usageCount += 1;
cacheItem.lastAccessed = performance.now();
cacheItem.value = value;
return;
}
if(this.cache.size >= this.capacity) {
this.evict();
}
this.cache.set(key, new LFUCacheItem(key, value));
};
LFUCache.prototype.evict = function() {
const minFrequency = new LFUCacheItem(0, 0, Infinity);
this.cache.forEach((cacheItem) => {
if(cacheItem.usageCount < minFrequency.usageCount) {
minFrequency.key = cacheItem.key;
minFrequency.usageCount = cacheItem.usageCount;
minFrequency.lastAccessed = cacheItem.lastAccessed;
} else if(cacheItem.usageCount == minFrequency.usageCount) {
if(cacheItem.lastAccessed < minFrequency.lastAccessed) {
minFrequency.key = cacheItem.key;
minFrequency.usageCount = cacheItem.usageCount;
minFrequency.lastAccessed = cacheItem.lastAccessed;
}
}
})
this.cache.delete(minFrequency.key);
}
结果
在提交后,发现最后几个测试用例超出时间限制了,然后返回题目描述查看,确实,有提到get和put方法的平均时间复杂度要为O(1)
返回查看代码会发现,get方法是没有问题的,主要是put方法,在加入缓存项时需要根据缓存容量和当前缓存size来决定是否需要evict不经常使用的缓存项,而我之前实现evict方法时是通过遍历所有缓存项并根据evict标准去删除不经常使用的缓存项,这属于暴力解决的方法,时间复杂度是O(n),不符合题目要求,因此会超时
再分析
根据上面结果已经推测出是evict方法的问题,要使evict删除不经常用的缓存项的时间复杂度为O(1),那就是不需要for循环遍历这步操作,要是缓存对象LFUCache本身就实时维护了minFrequency变量,用来实时维护使用频率最低的缓存项,那么在evict方法里便可以直接删除即可
现在问题就是:如何实现实时维护minFrequency变量?
惭愧,我在尝试了一番后,还是没能实现,实在是超出能力范围了,看了一下题解,有提到的方法:
- 哈希表
- 双向链表
- 红黑树
- 单调栈
- ......
结论
对于LFU 缓存这个打卡题,目前我是打不成功了,记录下来思路与需要去了解的知识,在掌握这些知识的某一天,再次来打卡~