LFU算法實現(460. LFU緩存)


今天字節客戶端三面問了這道題,沒做出來。第一,之前沒見過lfu,第二,要求O(1)時間,條件苛刻一點。只能說無緣字節。

言歸正傳,LFU算法:least frequently used,最近最不經常使用算法。

什么意思呢:對於每個條目,維護其使用次數cnt、最近使用時間time。

cache容量為n,即最多存儲n個條目。

那么當我需要插入新條目並且cache已經滿了的時候,需要刪除一個之前的條目。刪除的策略是:優先刪除使用次數cnt最小的那個條目,因為它最近最不經常使用,所以刪除它。如果使用次數cnt最小值為min_cnt,這個min_cnt對應的條目有多個,那么在這些條目中刪除最近使用時間time最早的那個條目(舉個栗子:a資源和b資源都使用了兩次,但a資源在5s的時候最后一次使用,b資源在7s的時候最后一次使用,那么刪除a,因為b資源更晚被使用,所以b資源相比a資源來說,更有理由繼續被使用,即時間局部性原理)。

 

類似lru算法的想法,利用哈希表加鏈表。鏈表是負責按時間先后排序的。哈希表是負責O(1)時間查找key對應節點的。

貼張圖,來源:https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/

還是用一個哈希表,用來O(1)時間查找key對應的節點。

另外由於lfu算法是按照兩個維度:引用計數、最近使用時間來排序的。所以一個鏈表肯定不夠用了。解決辦法就是按照下圖這樣,使用第二個哈希表,key是引用計數,value是一個鏈表,存儲使用次數為當前key的所有節點。該鏈表中的所有節點按照最近使用時間排序,最近使用的在鏈表頭部,最晚使用的在尾部。這樣我們可以完成O(1)時間查找key對應節點(通過第一個哈希表);O(1)時間刪除、更改某節點(通過第二個哈希表)。

 

注意:get(查詢)操作和put(插入)操作都算“使用”,都會增加引用計數。

所以get(key)操作實現思路:如果第一個哈希表中能查到key,那么取得相應鏈表節點。接下來在第二個哈希表中,把它移到其引用計數+1位置的鏈表頭部,並刪除之前的節點。

put(key,value)操作實現思路:如果第一個哈希表中能查找key,那么操作和get(key)一樣,只是把新節點的value置為新value。

如果查不到key,那么我們有可能需要刪除cache中的某一項(容量已經達到限制):直接找到第二個哈希表中最小引用計數的鏈表,刪除其末尾節點(最晚使用)。

之后再添加新節點即可。

 

注意點:1.容量超限需要刪除節點時,刪除了第二個哈希表中的項的同時,第一個哈希表中對應的映射也應該刪掉。

2.需要保持一個min_cnt整型變量用來保存當前的最小引用計數。因為容量超限需要刪除節點時,我們需要O(1)時間找到需要刪除的節點。

 

 

 

題目:

請你為 最不經常使用(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

 

解答:

struct p{
    int key,value,cnt;
    p(int a,int b,int c):key(a),value(b),cnt(c){}
};
class LFUCache {
public:
    unordered_map<int,list<p>::iterator> mp_key;
    unordered_map<int,list<p>> mp_cnt;
    int min_cnt=1;
    //key,value,cnts
    int n;
    LFUCache(int capacity) {
        n=capacity;
    }
    
    int get(int key) {
        if(n==0){return -1;}
        //cout<<"get begin  ";
        if(mp_key.count(key)){//命中
            auto iter=mp_key[key];
            int cnt=iter->cnt,val=iter->value;
            mp_cnt[cnt+1].push_front(p(iter->key,val,cnt+1));//放到cnt+1的鏈表頭部
            mp_cnt[cnt].erase(iter);    //刪除之前節點
            if(min_cnt==cnt and mp_cnt[cnt].size()==0){//更新min_cnt
                min_cnt++;
            }
            mp_key[key]=mp_cnt[cnt+1].begin();  //更新mp_key
            //cout<<"get end"<<endl;
            return val;
        }
        //cout<<"get end"<<endl;
        return -1;
    }
    
    void put(int key, int value) {
        if(n==0){return;}
        //cout<<"get begin  ";
        if(mp_key.count(key)){//命中
            auto iter=mp_key[key];
            int cnt=iter->cnt;
            mp_cnt[cnt+1].push_front(p(key,value,cnt+1));
            mp_cnt[cnt].erase(iter);
            mp_key[key]=mp_cnt[cnt+1].begin();
            if(min_cnt==cnt and mp_cnt[cnt].size()==0){
                min_cnt++;
            }
        }
        else{//插入新節點
            if(mp_key.size()>=n){//需要刪除cnt最小的list中最早出現的(mp_cnt[min_cnt].back())
                int deleteKey=mp_cnt[min_cnt].back().key;
                mp_cnt[min_cnt].pop_back();
                mp_key.erase(deleteKey);
            }
            mp_cnt[1].push_front(p(key,value,1));
            mp_key[key]=mp_cnt[1].begin();
            min_cnt=1;//插入新節點了,最小cnt一定是1
        }
        //cout<<"put end"<<endl;
    }
};

 


免責聲明!

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



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