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);
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM