算法---LeetCode 460. LFU 缓存

1. 题目

原题链接

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

LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之
前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。

注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。

示例:

输入:
[“LFUCache”, “put”, “put”, “get”, “put”, “get”, “get”, “put”, “get”, “get”, “g
et”]
[[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]

提示:
0 <= capacity, key, value <= 104
最多调用 105 次 get 和 put 方法
进阶:你可以为这两种操作设计时间复杂度为 O(1) 的实现吗?
Related Topics 设计
403 0

2. 题解

主要思路及理解

实现数据结构: 需要使用 两个哈希表再加上N个双链表才能实现
第一个哈希表中 存储了

HashMap cache 存缓存的内容; 即为下图中的 key-value哈希表
minFreq 是最小访问频次;
HashMap freqMap 存每个访问频次对应的 Node 的双向链表, 即下图中的频率哈希表

完整的结构如下图所示:

算法---LeetCode 460. LFU 缓存_第1张图片

具体操作

get操作的具体逻辑大致是这样:

  • 如果key不存在则返回-1
  • 如果key存在,则返回对应的value,同时:
    • 将元素的访问频率+1
      • 将元素从访问频率i的链表中移除,放到频率i+1的链表中
      • 如果频率i的链表为空,则从频率哈希表中移除这个链表

put操作就要复杂多了,大致包括下面几种情况

  • 如果key已经存在,修改对应的value,并将访问频率+1
    • 将元素从访问频率i的链表中移除,放到频率i+1的链表中
    • 如果频率i的链表为空,则从频率哈希表中移除这个链表
  • 如果key不存在
    • 缓存超过最大容量,则先删除访问频率最低的元素,再插入新元素
      • 新元素的访问频率为1,如果频率哈希表中不存在对应的链表需要创建
    • 缓存没有超过最大容量,则插入新元素
      • 新元素的访问频率为1,如果频率哈希表中不存在对应的链表需要创建

在代码实现中还需要维护一个minFreq的变量,用来记录LFU缓存中频率最小的元素,在缓存满的时候,可以快速定位到最小频繁的链表,以达到 O(1) 时间复杂度删除一个元素。
具体做法是:

  • 更新/查找的时候,将元素频率+1,之后如果minFreq不在频率哈希表中了,说明频率哈希表中已经没有元素了,那么minFreq需要+1,否则minFreq不变。
  • 插入的时候,这个简单,因为新元素的频率都是1,所以只需要将minFreq改为1即可。

以上参考: 超详细图解+动图演示 460. LFU缓存

代码实现

代码参考了 算法就像搭乐高:带你手撸 LFU 算法, 其主要思想与前文基本一致, 在实现的数据结构上进行了小改动, 从而极大减少了代码量

代码说明

LRU 算法的核心数据结构是使用哈希链表 LinkedHashMap,首先借助链表的有序性使得链表元素维持插入顺序,同时借助哈希映射的快速访问能力使得我们可以在 O(1) 时间访问链表的任意元素。

其在实现时使用了 三个 map, 分别是

HashMap keyToVal; key 到 val 的映射,我们后文称为 KV 表

HashMap keyToFreq; key 到 freq 的映射,我们后文称为 KF 表

HashMap> freqToKeys; freq 到 key 列表的映射,我们后文称为 FK 表

    class LFUCache {

        HashMap<Integer, Integer> keyToVal; // 存储键值对, key 到 val 的映射,我们后文称为 KV 表
        HashMap<Integer, Integer> keyToFreq; // 存储key和当前key的使用频率, key 到 freq 的映射,我们后文称为 KF 表
        HashMap<Integer, LinkedHashSet<Integer>> freqToKeys; // 一个频率可能对应多个key,存储频率与key的一对多映射, FK 表
        int cap; // LFU缓存最大容量
        int minFreq; // 记录当前最小使用频率

        public LFUCache(int capacity) {
            keyToVal = new HashMap();
            keyToFreq = new HashMap();
            freqToKeys = new HashMap();
            this.cap = capacity;
            this.minFreq = 0;
        }

        public int get(int key) {
            if (keyToVal.containsKey(key)) {
                // 先更新当前key的使用频率再返回val
                increaseFreq(key);
                return keyToVal.get(key);
            } else return -1; // 不存在当前key

        }

        public void put(int key, int value) {
            if (this.cap <= 0) return; // 如果当前最大容量小于等于0,则直接返回null

            // put时分两种情况:1. key存在,2.key不存在
            if (keyToVal.containsKey(key)) { // 1. key存在
                keyToVal.put(key, value);
                increaseFreq(key); // 更新key对应的频率+1
                return;
            }
            // 2.key不存在
            if (keyToVal.size() >= this.cap) { // 当前缓存已满
                removeMinFreqKey(); // 删除使用频率最小,有多个时删除数据最老的
            }
            keyToVal.put(key, value); //插入FV表
            keyToFreq.put(key, 1); // 插入FK表
            freqToKeys.putIfAbsent(1, new LinkedHashSet<Integer>()); // 确保1对应的链表存在
            freqToKeys.get(1).add(key); // 加入频率为1对应的链表中
            // 由于是新加入的, 因此minFreq 更新为1(最小就为1)
            this.minFreq = 1;

        }

        public void increaseFreq(int key) {
            int freq = keyToFreq.get(key);
            keyToFreq.put(key, freq + 1); // 更新kF表
            // 更新FK表
            freqToKeys.get(freq).remove(key);
            freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<Integer>());
            freqToKeys.get(freq + 1).add(key);
            // 判断freq对应的链表是否没有元素了
            if (freqToKeys.get(freq).isEmpty()) {
                freqToKeys.remove(freq);
                if (freq == this.minFreq) { // 如果freq正好是minFreq, 则更新minFreq
                    this.minFreq++;
                }
            }
        }

        public void removeMinFreqKey() {
            // 先获取到minFreq对应的链表
            LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);
            // 由于链表是有序的, 因此第一个加入的一定是频率最小且存在时间最久的那个
            int deleteKey = keyList.iterator().next();
            keyList.remove(deleteKey); // 更新Fk表
            if (keyList.isEmpty()) { //  如果当前minFreq对应的链表已经没有元素了
                freqToKeys.remove(this.minFreq); // 移除当前minFreq对应的链表
                // 注:在此不用再更新minFreq, 因为每次put时更新了minFreq为1
            }
            keyToVal.remove(deleteKey); // 更新 kV 表
            keyToFreq.remove(deleteKey); // 更新 kF 表
        }
    }

详细代码说明可以参考链接: 算法就像搭乐高:带你手撸 LFU 算法

你可能感兴趣的:(算法,链表,算法,数据结构,java)