【LeetCode刷题】NO.40---第460题

231 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

一.题目

460. LFU 缓存 请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
  • int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
  • void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。 为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。 当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 示例:
输入:
["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lfu = new LFUCache(2);
lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lfu.get(1);      // 返回 1
                 // cache=[1,2], cnt(2)=1, cnt(1)=2
lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                 // cache=[3,1], cnt(3)=1, cnt(1)=2
lfu.get(2);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,1], cnt(3)=2, cnt(1)=2
lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                 // cache=[4,3], cnt(4)=1, cnt(3)=2
lfu.get(1);      // 返回 -1(未找到)
lfu.get(3);      // 返回 3
                 // cache=[3,4], cnt(4)=1, cnt(3)=3
lfu.get(4);      // 返回 4
                 // cache=[3,4], cnt(4)=2, cnt(3)=3

提示:

  • 0 <= capacity <= 104
  • 0 <= key <= 105
  • 0 <= value <= 109
  • 最多调用 2 * 105 次 get 和 put 方法

二、思路分析:

首先明确几个点

  1. 调用get方法的时候,如果键key存在需要返回对应的值。
  2. 只要用到了get或者put方法,就用对访问的键key的计数器+1get方法访问缓存中有要查询的key)。
  3. 如果缓存容量满了的话,我们需要将访问最少的键key删除,如果存在多个那么删除最长时间未用的。 解决的思路
  • 对于问题1,我们采取Map数据结构存储键值对。
  • 对于问题2,我们在创建一个Map来存储键key以及它的访问次数count
  • 问题3中我们需要找到最小访问次数的key值,所以我们为了时间复杂度的降低我们每次更新次数都记录最小的次数以便后续直接删除对应的key,但是由于最小次数可能有需要的键值对应,所以我们需要在创建一个set数据结构用来存储各个次数的键群,每个用到get操作的时候我们都需要更新一次该键的对应的键-次数数据结构以及次数对应的键群。

而对于put操作,如果有存在key,那么我们直接做出修改更新次数对应的键群和key-次数键值对,如果没有存在那么我们还要判断缓存容量是否慢了,如果满了我们就需要先删除最小次数的最老键,通过最小次数获取键群,然后利用产生键群的遍历群,键群的第一个元素就是最老的我们用next().value获得,删除该键值对应的所有键值对,然后新增我们要新增的键值对即可。

三、代码:

/**
 * @param {number} capacity
 */
var LFUCache = function(capacity) {
    this.capacity = capacity
    this.mincount = 0
    this.cache = new Map()
    //创建key-count键值对
    this.keytocount = new Map()
    //创建count-keys键值对
    this.counttokeys = new Map()
};

/** 
 * @param {number} key
 * @return {number}
 */
LFUCache.prototype.get = function(key) {
    if(this.cache.has(key)){
        this.incrementTime(key)
        return this.cache.get(key)
    }
    return -1
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LFUCache.prototype.put = function(key, value) {
    if(this.capacity === 0) return
    if(this.cache.has(key)){
        //缓存中有要插入的key直接修改
        this.cache.set(key,value)
        this.incrementTime(key)
    }else{
        //缓存中没有对应的key
        if(this.cache.size >= this.capacity){
            //超过缓存容量且没有对应key进行逐出最小最老键值对操作
            //获取最小次数的键群
            let Minset = this.counttokeys.get(this.mincount)
            let Minkey = Minset.keys().next().value
            Minset.delete(Minkey)
            this.cache.delete(Minkey)
            this.keytocount.delete(Minkey)
        }
        //新增键值对
        this.cache.set(key,value)
        //获取次数为1的键群
        let useSet = this.counttokeys.get(1)
        if(!useSet){
            //不存在创建就是了
            useSet = new Set()
            this.counttokeys.set(1,useSet)
        }
        this.keytocount.set(key,1)
        useSet.add(key)
        this.mincount = 1
    }
};
LFUCache.prototype.incrementTime= function(key){
    let time = this.keytocount.get(key)
    let useSet = this.counttokeys.get(time)
    //次数为1的键群空的时候需要做出判断
    if(this.mincount === time && useSet.size === 1){
        this.mincount++
    }
    useSet.delete(key)
    this.keytocount.set(key,time + 1)
    useSet = this.counttokeys.get(time + 1)
    if(!useSet){
        useSet = new Set()
        this.counttokeys.set(time + 1,useSet)
    }
    useSet.add(key)
}

四、总结:

这道题目是面试经常会问到的题目,因为这个算法贴合实际,LFU缓存淘汰算法在工程实践中经常使用。这道题我们特别需要注意我们创建的每个MAP数据结构的含义,每次进行键值对操作的时候不能够忘记对每个Map的处理,以及如何更新最小次数这个细节。