1. 程式人生 > 實用技巧 >演算法就像搭樂高:帶你手擼 LFU 演算法

演算法就像搭樂高:帶你手擼 LFU 演算法

層層拆解,帶你手寫 LFU 演算法

讀完本文,你不僅學會了演算法套路,還可以順便去 LeetCode 上拿下如下題目:

460.LFU快取機制

-----------

上篇文章 帶你手寫LRU演算法 寫了 LRU 快取淘汰演算法的實現方法,本文來寫另一個著名的快取淘汰演算法:LFU 演算法。

LRU 演算法的淘汰策略是 Least Recently Used,也就是每次淘汰那些最久沒被使用的資料;而 LFU 演算法的淘汰策略是 Least Frequently Used,也就是每次淘汰那些使用次數最少的資料。

LRU 演算法的核心資料結構是使用雜湊連結串列 LinkedHashMap,首先借助連結串列的有序性使得連結串列元素維持插入順序,同時藉助雜湊對映的快速訪問能力使得我們可以在 O(1) 時間訪問連結串列的任意元素。

從實現難度上來說,LFU 演算法的難度大於 LRU 演算法,因為 LRU 演算法相當於把資料按照時間排序,這個需求借助連結串列很自然就能實現,你一直從連結串列頭部加入元素的話,越靠近頭部的元素就是新的資料,越靠近尾部的元素就是舊的資料,我們進行快取淘汰的時候只要簡單地將尾部的元素淘汰掉就行了。

而 LFU 演算法相當於是把資料按照訪問頻次進行排序,這個需求恐怕沒有那麼簡單,而且還有一種情況,如果多個數據擁有相同的訪問頻次,我們就得刪除最早插入的那個資料。也就是說 LFU 演算法是淘汰訪問頻次最低的資料,如果訪問頻次最低的資料有多條,需要淘汰最舊的資料。

所以說 LFU 演算法是要複雜很多的,而且經常出現在面試中,因為 LFU 快取淘汰演算法在工程實踐中經常使用,也有可能是應該 LRU 演算法太簡單了。不過話說回來,這種著名的演算法的套路都是固定的,關鍵是由於邏輯較複雜,不容易寫出漂亮且沒有 bug 的程式碼

那麼本文 labuladong 就帶你拆解 LFU 演算法,自頂向下,逐步求精,就是解決複雜問題的不二法門。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

一、演算法描述

要求你寫一個類,接受一個 capacity 引數,實現 getput 方法:

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,該 keyfreq 就要加一。

3、如果在容量滿了的時候進行插入,則需要將 freq 最小的 key 刪除,如果最小的 freq 對應多個 key,則刪除其中最舊的那一個。

好的,我們希望能夠在 O(1) 的時間內解決這些需求,可以使用基本資料結構來逐個擊破:

1、使用一個 HashMap 儲存 keyval 的對映,就可以快速計算 get(key)

HashMap<Integer, Integer> keyToVal;

2、使用一個 HashMap 儲存 keyfreq 的對映,就可以快速操作 key 對應的 freq

HashMap<Integer, Integer> 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 這個需求。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

綜上,我們可以寫出 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) {}

}

三、程式碼框架

LFU 的邏輯不難理解,但是寫程式碼實現並不容易,因為你看我們要維護 KV 表,KF 表,FK 表三個對映,特別容易出錯。對於這種情況,labuladong 教你三個技巧:

1、不要企圖上來就實現演算法的所有細節,而應該自頂向下,逐步求精,先寫清楚主函式的邏輯框架,然後再一步步實現細節。

2、搞清楚對映關係,如果我們更新了某個 key 對應的 freq,那麼就要同步修改 KF 表和 FK 表,這樣才不會出問題。

3、畫圖,畫圖,畫圖,重要的話說三遍,把邏輯比較複雜的部分用流程圖畫出來,然後根據圖來寫程式碼,可以極大減少出錯的概率。

下面我們先來實現 get(key) 方法,邏輯很簡單,返回 key 對應的 val,然後增加 key 對應的 freq

相關推薦:

_____________

本文只能在 labuladong 公眾號檢視,關注後可直接搜尋本站內容: