力扣 | 460. LFU 缓存

696 阅读3分钟

引言

今日的算法打卡题是一道困难题,毫无悬念,没有做出来,但是还是记录一下自己的思考过程,以及需要去学习和掌握的知识点

题目描述 image.png

过程

分析

在看完题目描述之后,我接着便是好好研究了一下它提供的示例:

image.png

看完之后,有了大概的思路:

  1. 需要两个对象,LFUCacheItem对象表示每个缓存项;LFUCache对象则是用来维护所有缓存项的容器
  2. LFUCacheItem对象包括的属性:
    • key:键
    • value:值
    • usageCount:缓存项使用次数计数器
    • lastAccessed: 最后一次访问缓存项的时间
  3. LFUCache对象包括的属性:
    • capacity:缓存的最大容量
    • cache:一个字典,用于将键映射到对应缓存项(LFUCacheItem对象)
  4. 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);
}

结果

在提交后,发现最后几个测试用例超出时间限制了,然后返回题目描述查看,确实,有提到getput方法的平均时间复杂度要为O(1)

image.png image.png

返回查看代码会发现,get方法是没有问题的,主要是put方法,在加入缓存项时需要根据缓存容量和当前缓存size来决定是否需要evict不经常使用的缓存项,而我之前实现evict方法时是通过遍历所有缓存项并根据evict标准去删除不经常使用的缓存项,这属于暴力解决的方法,时间复杂度是O(n),不符合题目要求,因此会超时

再分析

根据上面结果已经推测出是evict方法的问题,要使evict删除不经常用的缓存项的时间复杂度为O(1),那就是不需要for循环遍历这步操作,要是缓存对象LFUCache本身就实时维护了minFrequency变量,用来实时维护使用频率最低的缓存项,那么在evict方法里便可以直接删除即可

现在问题就是:如何实现实时维护minFrequency变量?

惭愧,我在尝试了一番后,还是没能实现,实在是超出能力范围了,看了一下题解,有提到的方法:

  • 哈希表
  • 双向链表
  • 红黑树
  • 单调栈
  • ......

结论

对于LFU 缓存这个打卡题,目前我是打不成功了,记录下来思路与需要去了解的知识,在掌握这些知识的某一天,再次来打卡~