LFU算法详解

LFU算法详解

文章参考东哥文章:算法题就像搭乐高:手把手带你拆解 LFU 算法 (qq.com)

一、算法描述

要求你写一个类,接受一个capacity参数,实现get和put方法

class LFUCache {
    // 构造容量为 capacity 的缓存
    public LFUCache(int capacity) {}
    // 在缓存中查询 key
    public int get(int key) {}
    // 将 key 和 val 存入缓存
    public void put(int key, int val) {}
}

get(key)方法会去缓存中查询键key,如果key存在,则返回key对应的val,否则返回 -1。

put(key, value)方法插入或修改缓存。如果key已存在,则将它对应的值改为val;如果key不存在,则插入键值对(key, val)

当缓存达到容量capacity时,则应该在插入新的键值对之前,删除使用频次(后文用freq表示)最低的键值对。如果freq最低的键值对有多个,则删除其中最旧的那个。

// 构造一个容量为 2 的 LFU 缓存
LFUCache cache = new LFUCache(2);

// 插入两对 (key, val),对应的 freq 为 1
cache.put(1, 10);
cache.put(2, 20);

// 查询 key 为 1 对应的 val
// 返回 10,同时键 1 对应的 freq 变为 2
cache.get(1);

// 容量已满,淘汰 freq 最小的键 2
// 插入键值对 (3, 30),对应的 freq 为 1
cache.put(3, 30);   

// 键 2 已经被淘汰删除,返回 -1
cache.get(2); 

二、思路分析

先从简单的开始,根据LFU算法的逻辑,我们先列举出算法执行过程中几个简单的事实。

算法需求

1.调用get(key)方法时,返回key对应的 val.

2.只要用get或者put 方法访问某个key的时候,该key的freq+1.

3.如果容器满了,在执行put方法时,要先删除freq最小的key(如果最小key有多个,删除最早访问过的key)。

如果我们希望put和get 能够在O(1)时间内完成,可以用基本数据结构来逐个击破。

逐个分析

1.get方法:使用HashMap储存key到val的映射,可以快速计算出get(key)

HashMap<Interger, Interger> KeyToVal

2.使用HashMap储存key到freq的映射,可以快速计算出操作key对应的freq

HashMap<Interger, Interger> KeyToFreq

3、这个需求应该是 LFU 算法的核心,所以我们分开说。

3.1、首先,肯定是需要freqkey的映射,用来找到freq最小的key

3.2、freq最小的key删除,那你就得快速得到当前所有key最小的freq是多少。想要时间复杂度 O(1) 的话,肯定不能遍历一遍去找,那就用一个变量minFreq来记录当前最小的freq吧。

3.3、可能有多个key拥有相同的freq,所以 freqkey是一对多的关系,即一个freq对应一个key的列表。

3.4、希望freq对应的key的列表是存在时序的,便于快速查找并删除最旧的key

3.5、希望能够快速删除key列表中的任何一个key,因为如果频次为freq的某个key被访问,那么它的频次就会变成freq+1,就应该从freq对应的key列表中删除,加到freq+1对应的key的列表中。

HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
int minFreq = 0;

介绍一下这个LinkedHashSet,它满足我们 3.3,3.4,3.5 这几个要求。你会发现普通的链表LinkedList能够满足 3.3,3.4 这两个要求,但是由于普通链表不能快速访问链表中的某一个节点,所以无法满足 3.5 的要求。

LinkedHashSet顾名思义,是链表和哈希集合的结合体。链表不能快速访问链表节点,但是插入元素具有时序;哈希集合中的元素无序,但是可以对元素进行快速的访问和删除。

那么,它俩结合起来就兼具了哈希集合和链表的特性,既可以在 O(1) 时间内访问或删除其中的元素,又可以保持插入的时序,高效实现 3.5 这个需求。

综上,我们可以写出 LFU 算法的基本数据结构:

class LFUCache {
    // key 到 val 的映射,我们后文称为 KV 表
    HashMap<Integer, Integer> keyToVal;
    // key 到 freq 的映射,我们后文称为 KF 表
    HashMap<Integer, Integer> keyToFreq;
    // freq 到 key 列表的映射,我们后文称为 FK 表
    HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
    // 记录最小的频次
    int minFreq;
    // 记录 LFU 缓存的最大容量
    int cap;

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

    public int get(int key) {}

    public void put(int key, int val) {}

}

get(int key)方法

第一步需要判断key是否存在,如果不存在,返回-1,如果存在,需要将key的freq+1,再返回key对应的value。

    public int get(int key) {
        if (keyToValue.containsKey(key)) {
            //存在key;访问频率+1,返回值
            increaseFrequency(key);
            return keyToValue.get(key);

        } else {
            return -1;
        }
    }
increaseFrequency(int key)方法

这里的increaseFrequency(int key)方法逐步的操作为

1.拿到key对应的KeyFrequency;

2.将key对应的KeyFrequency+1;

3.将frequencyToKey中KeyFrequency对应的LinkedHashSet中删除key元素;

4.将frequencyToKey中KeyFrequency+1对应的LinkedHashSet添加key元素;(如果frequencyToKey中不存在KeyFrequency+1,则需要添加KeyFrequency+1);

5.如果KeyFrequency对应的LinkedHashSet为空,则在frequencyToKey中删除KeyFrequency;

6.如果minFreq与 keyFrequency相等,则minFreq++

private void increaseFrequency(int key) {
    int keyFrequency = keyToFrequency.get(key);
    keyToFrequency.put(key, keyFrequency + 1);
    frequencyToKey.get(keyFrequency).remove(key);
    frequencyToKey.putIfAbsent(keyFrequency + 1, new LinkedHashSet<>());
    frequencyToKey.get(keyFrequency + 1).add(key);
    if (frequencyToKey.get(keyFrequency).isEmpty()) {
        frequencyToKey.remove(keyFrequency);
    }
    if (this.minFreq == keyFrequency) {
        this.minFreq++;
    }
}

put(int key, int value)方法

1、判断是否存在key

如果存在:(1)更新keyToValue。(2)增加key的frequency。

2、如果不存在:判断capacity是否满了:

如果满了:(1)移除最小frequency的key

​ (2)put

​ (3)让minFreq =1;

如果没满:(1)put

​ (2)让minFreq =1;

    public void put(int key, int value) {
        //第一步,判断LFUCache中是否有key;
        if (keyToValue.containsKey(key)) {
            //如果有key,修改值,访问频率+1;
            keyToValue.put(key, value);
            increaseFrequency(key);
            return;
        }
        //第二步:如果不存在,查询capacity是否已经满了
        //如果已经满了,需要先删除最小使用频率的key,再添加新的key-value键值对
        if (keyToValue.size() >= this.capacity) {
            removeMinFrequency();
        }
            keyToValue.put(key, value);
            keyToFrequency.put(key, 1);
            frequencyToKey.putIfAbsent(1, new LinkedHashSet<>());
            frequencyToKey.get(1).add(key);
            this.minFreq = 1;
    }
removeMinFrequency()方法:
    private void removeMinFrequency() {
        LinkedHashSet<Integer> keyList = frequencyToKey.get(this.minFreq);
        int removeKey = keyList.iterator().next();
        keyList.remove(removeKey);
        if(keyList.isEmpty()){
            frequencyToKey.remove(this.minFreq);
        }
        keyToValue.remove(removeKey);
        keyToFrequency.remove(removeKey);
    }
posted @ 2021-09-01 16:20  Code_Red  阅读(1191)  评论(0编辑  收藏  举报