LRU/LFU

LRU 缓存

LRU简介

LRU(Least Recently Used)是一种常见的缓存淘汰策略,用于在有限的内存空间中存储最近最常使用的数据。当缓存达到其容量上限时,最近最少使用的数据将从缓存中淘汰。
缺点:
对于偶发的批量操作,比如批量查询历史数据,就有可能使缓存中热门数据被这些历史数据替换,造成缓存污染,导致缓存命中率下降,减慢了正常数据查询。

代码实现哈希表+双向链表

class LRUCache {
public:
    struct ListNode{
        int key,value;
        ListNode *next;
        ListNode *prev;
        ListNode():key(0),value(0),next(nullptr),prev(nullptr){}
        ListNode(int k,int v):key(k),value(v),next(nullptr),prev(nullptr){}
        ListNode(int k,int v,ListNode*next,ListNode *prev):key(k),value(v),next(next),prev(prev){}
    };
    LRUCache(int capacity):size(capacity){
        vhead = new ListNode();
        tail = new ListNode();
        vhead->next = tail;
        tail->prev=vhead;
    }
    
    int get(int key) {
        auto it=ha.find(key);
        if(it!=ha.end()) {
            ListNode *te = it->second;
            if(vhead->next!=te){
                del_inser(te,vhead);
            }
            return te->value;
        }
        return -1;
    }
    
    void put(int key, int value) {
        auto it=ha.find(key);
        if(it!=ha.end()) {
            ListNode *te = it->second;
            te->value=value;
            del_inser(te,vhead);
        }
        else{
            if(ma.size()==size) {
                ListNode *te = tail->prev;
                auto it = ha.find(te->key);
                ha.erase(it);
                del(te);
                delete te;
                te=nullptr;
            }
            ListNode *node = new ListNode(key,value);
            insert(node,vhead); 
            ha.insert(pair<int,ListNode*>(key,node));
        }
    }
private:
    void insert(ListNode *node,ListNode *cur){
        node->next=cur->next;
        cur->next=node;
        node->prev=cur;
        node->next->prev=node; 
    }
    void del(ListNode *node){
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    void del_inser(ListNode *node,ListNode *cur){
        del(node);
        insert(node,cur);
    }
private:
    //hash表+单向链表
    int size;//容量
    unordered_map<int,ListNode*> ha;
    ListNode *vhead;//虚拟头节点
    ListNode *tail;
};

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

LFU缓存

LFU算法是一种基于访问频次的缓存淘汰算法,它会根据数据的历史访问频率来淘汰数据。LFU算法的优点是在数据访问符合正态分布时,相比于LRU算法,LFU算法的缓存命中率会高一些。但是LFU算法的复杂度要比LRU更高一些,需要维护数据的访问频次,每次访问都需要更新 。

哈希表+平衡树

首先需要定义一个节点的结构

struct node{
	int key,value,freq,time;
	node(int k,int v,int f,int t):key(k),value(v),freq(f),time(t){}
	bool operator<(const node&rhs){
		return freq==rhs.freq?time<rhs.time:freq<rhs.freq;
	}
};

哈希表用来存储key到node之间的映射关系,这样通过key找到node的时间复杂度为O(1)
平衡树set用来存储node,并且按照第一关键字为freq,第二关键字为time从小到大进行排序。

对于get操作,首先在哈希表中进行寻找若找不到则返回-1。找到的情况下,在平衡树中删除该节点修改freq和node的值,重新插入平衡树,修改哈希表中的映射关系,返回节点的value值。

对于put操作。首先在哈希表中寻找该节点,找不到:
若当前元素个数小于容量,构建新的节点并插入。元素个数等于容量,先根据规则删除元素也就是删除平衡树中的第一个节点,之后插入新的节点。找得到:修改value的值,接下来的操作和get操作一致

class LFUCache {
public:
    LFUCache(int capacity):cap(capacity),time(0){
        se.clear();
        ma.clear();
    }
    
    int get(int key) {
        if(cap==0) return -1;
        auto it = ma.find(key);
        if(it==ma.end()) return -1;
        node te=it->second;
        del_inser(se,ma,te);
        return te.value;
    }
    
    void put(int key, int value) {
        if(cap==0) return;
        auto it = ma.find(key);
        if(it!=ma.end()){
            node te=it->second;
            te.value = value;
            del_inser(se,ma,te);
        }else {
            if(ma.size()==cap){
                ma.erase(se.begin()->key);
                se.erase(se.begin());
            }
            node nd(key,value,1,++time);
            se.insert(nd);
            ma.insert(make_pair(key,nd));
        }
    }
private:
    struct node{
        int key,value,freq,time;
        node(){}
        node(int k,int v,int f,int t):key(k),value(v),freq(f),time(t){}
        bool operator<(const node&rhs)const{
            return freq!=rhs.freq?freq<rhs.freq:time<rhs.time;
        }
    };
    void del_inser(set<node>&se,unordered_map<int,node>&ma,node&it){
        se.erase(it);
        it.freq+=1;
        it.time=++time;
        se.insert(it);
        ma[it.key]=it;
    }
private:
    set<node>se;
    unordered_map<int,node>ma;
    int cap;
    int time;
};

/**
 * 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);
 */

时间复杂度分析。对于get操作,从哈希表中获取元素为O(1),从平衡树中删除更新重新插入的操作为O(logn)总体的时间复杂度为O(logn);对于put操作,O(logn)
空间复杂度分析,O(capcaity)

双哈希表

第一个哈希表key_table记录key与双向链表中节点所在的位置。第二个哈希表freq_table记录freq与对应双向链表之间的映射。一个freq对应一个双向链表是为了保证get和put操作的时间复杂度为O(1)。但是还要额外记录一个变量minfreq记录节点的最小使用频率。为了删除操作做准备。

对于get操作,首先根据key在key_table中找到对应的链表节点,找不到返回-1。找到的话,首先记录当前节点的freq并删除freq_table[freq]对应的链表中的该节点,删除之后还要判断当前链表是否为空,链表为空删除该链表,如果链表为空并且当前的freq==minfreq则将minfreq+1。然后根据key value freq+1重新构造一个新的节点并将其插入freq_table[freq+1]的链表的头部。

对于put操作,如果能够根据key在key_table中找到节点则跟get操作一样但是要修改节点的value值。如果没有找到,如果当前元素个数等于容量,则先删除freq_table[minfreq]链表的尾部节点,删除之后链表为空删除链表,之后新建节点(新建节点的freq为1)插入freq_table[1]的链表的头部,并将minfreq设置为1。

class LFUCache {
public:
    LFUCache(int capacity):minfreq(0),cap(capacity){
        key_table.clear();
        freq_table.clear();
    }
    
    int get(int key) {
        if(cap==0)return -1;
        auto it =key_table.find(key);
        if(it==key_table.end()) return -1;
        list<Node>::iterator node=it->second;
        int freq=node->freq;int val=node->value;
        freq_table[freq].erase(node);
        if(freq_table[freq].empty()){
            freq_table.erase(freq);
            if(minfreq==freq) ++minfreq;
        }
        //这里的代码一直报错说我的node数据类型不兼容,检查了好久才发现,我前面定义了一个
        //list::iterator node=it->second;与struct node发生冲突,而且局部变量对全局变量有覆盖作用,所以一直报错。所以将struct node改成struct Node;
        freq_table[freq+1].push_front(Node(key,val,freq+1));
        key_table[key]=freq_table[freq+1].begin();
        return val;
    }
    
    void put(int key, int value) {
        if(cap==0) return;
        auto it=key_table.find(key);
        if(it!=key_table.end()){
            list<Node>::iterator node=it->second;
            int freq=node->freq;
            freq_table[freq].erase(node);
            if(freq_table[freq].size()==0){
                freq_table.erase(freq);
                if(minfreq==freq) ++minfreq;
            }
            freq_table[freq+1].push_front(Node(key,value,freq+1));
            key_table[key]=freq_table[freq+1].begin();
        }else{
            if(cap==key_table.size()){
                auto it = freq_table[minfreq].back();
                key_table.erase(it.key);
                freq_table[minfreq].pop_back();
                if(freq_table[minfreq].empty()){
                    freq_table.erase(minfreq);
                }
            }
            minfreq=1;
            freq_table[1].push_front(Node(key,value,1));
            key_table[key]=freq_table[1].begin();
        }
    }
private:
    struct Node{
        int key,value,freq;
        Node(){}
        Node(int k,int v,int f):key(k),value(v),freq(f){}
    };
private:
    int minfreq,cap;
    unordered_map<int,list<Node>::iterator> key_table;
    unordered_map<int,list<Node>> freq_table;
};

/**
 * 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);
 */

总结

LRU算法更加关注数据的访问时间,而LFU算法更加关注数据的访问频率。因此,在实际应用中,需要根据具体的业务场景来选择合适的缓存淘汰算法。

你可能感兴趣的:(每日一题,算法,缓存,C++,链表,平衡树,哈希表)