1. 程式人生 > 實用技巧 >【LeetCode-模擬】LFU快取

【LeetCode-模擬】LFU快取

題目描述

請你為 最不經常使用(LFU)快取演算法設計並實現資料結構。它應該支援以下操作:get 和 put。

  • get(key) - 如果鍵存在於快取中,則獲取鍵的值(總是正數),否則返回 -1。
  • put(key, value) - 如果鍵已存在,則變更其值;如果鍵不存在,請插入鍵值對。當快取達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除最久未使用的鍵。

「項的使用次數」就是自插入該項以來對其呼叫 get 和 put 函式的次數之和。使用次數會在對應項被移除後置為 0 。

進階:
你是否可以在 O(1) 時間複雜度內執行兩項操作?

示例:

LFUCache cache = new LFUCache( 2 /* capacity (快取容量) */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 去除 key 2
cache.get(2);       // 返回 -1 (未找到key 2)
cache.get(3);       // 返回 3
cache.put(4, 4);    // 去除 key 1
cache.get(1);       // 返回 -1 (未找到 key 1)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

題目連結: https://leetcode-cn.com/problems/lfu-cache/

思路

這題和LRU快取機制很像。
LRU

  • 快取滿時,刪除最久未使用的元素;

LFU:

  • 快取滿時,刪除使用頻次最少的元素;

所以,在 LFU 中,我們還需要記錄每個元素被訪問的次數,每個元素除了 key,val 之外,還需要定義 freq 表示訪問的次數:

struct Node{
    int key;
    int val;
    int freq;

    Node(int key, int val, int freq):key(key), val(val), freq(freq){}
};

我們使用兩個雜湊表:unordered_map<int, list<Node>::iterator> keyTable 用來儲存 key 到連結串列節點指標的對映;unordered_map<int, list<Node>> freqTable 用來儲存頻數 freq 到連結串列的對映,具體如下圖:

圖來自這篇題解,左邊的雜湊表為 keyTable,右邊的雜湊表為 freqTale。可以看到 freqTable 根據頻數儲存了具體的連結串列,而 keyTable 儲存了 key 到連結串列節點地址的對映。

除此之外,我們還需要一個變數 minFreq 來儲存目前最少訪問的次數,通過 freqTable[minFreq].pop_back() 刪除使用最少的節點。

演算法步驟:

  • get(key):

    • 如果 key 不在 keyTable 中,返回 -1;
    • 否則,通過 keyTable[key] 獲取 key 對應的節點地址 it,然後通過 it 得到節點的 key、val、freq,將節點從 freqTable[freq] 對應的連結串列刪除,然後將該節點加入到 freqTable[freq+1] 對應的連結串列頭;
  • put(key, value):

    • 如果 key 在 keyTable 中,則使用和 get(key) 中的第二步類似的方法;
    • 否則,如果快取已經滿了,則根據 minFreq 刪除使用最少的節點,然後設定 minFreq 為 1,將新的節點放入 freqTable[1] 對應的連結串列頭。

具體程式碼如下:

struct Node{
    int key;
    int val;
    int freq;

    Node(int key, int val, int freq):key(key), val(val), freq(freq){}
};

class LFUCache {
private:
    unordered_map<int, list<Node>::iterator> keyTable;
    unordered_map<int, list<Node>> freqTable;
    int capacity;
    int minFreq;
public:
    LFUCache(int capacity) {
        this->capacity = capacity;
        this->minFreq = 0;
    }
    
    int get(int key) {
        if(keyTable.count(key)==0) return -1;
        else{
            auto it = keyTable[key];   // it 為 key 對應的節點地址
            int val = it->val;
            int freq = it->freq;
            freqTable[freq].erase(it);  // 在 freqTable[freq] 對應的連結串列中刪除節點
            if(freqTable[freq].size()==0){  // 如果刪除後 freqTable[freq] 為空
                freqTable.erase(freq);
                if(minFreq==freq) minFreq++; // 注意這一步
            }
            freqTable[freq+1].push_front(Node(key, val, freq+1)); // 將節點放入 freqTable[freq+1] 對應的連結串列中
            keyTable[key] = freqTable[freq+1].begin();
            return val;
        }
    }
    
    void put(int key, int value) {
        if(this->capacity==0) return;   // 注意判斷容量是否為 0
        if(keyTable.count(key)!=0){    // key 已經在快取中了
            auto it = keyTable[key];   // 下面的步驟和 get 函式中 else 部分基本相同
            int freq = it->freq;
            freqTable[freq].erase(it);
            if(freqTable[freq].size()==0){
                freqTable.erase(freq);
                if(minFreq==freq) minFreq++;
            }
            freqTable[freq+1].push_front(Node(key, value, freq+1));
            keyTable[key] = freqTable[freq+1].begin();
        }
        else{                          // key 不在快取中
            if(keyTable.size()==this->capacity){   // 快取容量已滿
                Node node = freqTable[minFreq].back();   // 通過 minFreq 找到使用最少的節點 back()
                keyTable.erase(node.key);      // 刪除使用最少的節點
                freqTable[minFreq].pop_back();
                if(freqTable[minFreq].empty()){
                    freqTable.erase(minFreq);
                }
            }
            freqTable[1].push_front(Node(key, value, 1));
            keyTable[key] = freqTable[1].begin();
            minFreq = 1;   // minFreq 置為 1
        }
    }
};

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache* obj = new LFUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

注意點:

  • 在 freqTable[freq] 對應的連結串列中插入節點時,都是插入連結串列頭 (push_front());
  • 每次在 freqTable[freq] 對應的連結串列中刪除節點後,都要判斷 freqTable[freq] 是否為空,如果為空,則將 freq 從 freqTable 中刪除,並在必要的情況下更新 minFreq。

參考

1、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-shuang-xiang-lian-biao-lfuhuan-cun-by-realzz/
2、https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/