缓存策略之LRU和LFU

作者: FindCrt | 来源:发表于2018-09-11 16:06 被阅读5次

为什么

缓存,就是把数据存储在本地,简单实用key-value做一个映射就好了。但为什么还要缓存策略,因为缓存的大小需要限制,否则存储的东西只增不减,时间一长就有很多内存浪费。

因为资源总是有限,所以优化,因为优化所以复杂

LRU

这个是least recently used的缩写,即最近最少使用。它的逻辑很简单:一串数据,A->B->C->D->....->head,新的数据来就压到头部,哪个数据用到了也拉到头部,然后内存到了限制额,就丢掉最后的那个。

也就是最近被用到得越多,不管是存还是取,就更能活下来

class LRUCache {
    struct CacheNode{
        int key;
        int value;
        
        CacheNode *pre = nullptr;
        CacheNode *next = nullptr;
        
        CacheNode(int key = 0, int value = 0):key(key),value(value){};
    };
    
    //头和尾各设了一个哨兵指针,方便处理,不用做头尾的判空
    CacheNode *head = new CacheNode();
    CacheNode *tail = new CacheNode();
    
    int capacity;
    int size = 0;
    
    unordered_map<int,CacheNode *> store;
    
    /** 在头部插入一个数据 */
    inline void insertHead(CacheNode *node){
        node->next = head;
        node->pre = head->pre;
        
        node->pre->next = node;
        node->next->pre = node;
    }
    
    inline void unbind(CacheNode *node){
        node->next->pre = node->pre;
        node->pre->next = node->next;
    }
    
    /** 把节点拉到最前面 */
    inline void forword(CacheNode *node){
        if (node->next == head) { //已经是最前面了
            return;
        }
        unbind(node);
        insertHead(node);
    }
    
    /** 扔掉最后一个 */
    inline void dropTail(){
        auto drop = tail->next;
        unbind(drop);
        store.erase(drop->key);
        size--;
        delete drop;
    }
    
    friend ostream& operator<<(ostream& os, LRUCache &cache){
        auto cur = cache.tail->next;
        while (cur != cache.head) {
            os<<cur->key<<"->";
            cur = cur->next;
        }
        
        return os;
    }
    
public:
    
    LRUCache(int capacity) {
        assert(capacity); //容量等于0没法玩
        this->capacity = capacity;
        
        head->pre = tail;
        tail->next = head;
    }

    int get(int key) {
        if (store.find(key) == store.end()) {
            return -1;
        }else{
            auto find = store[key];
            forword(find);  //找到内部存在的数据,提到最前面
            return find->value;
        }
    }
    
    void set(int key, int value) {
        if (store.find(key) == store.end()) {
            //位置1:内存满了,丢弃一个
            if (size == capacity) {
                dropTail();
            }
            
            auto node = new CacheNode(key, value);
            store[key] = node;
            insertHead(node);
            size++;
        }else{
            //内存已有对应的key,更新value,然后提到最前面
            auto find = store[key];
            find->value = value;
            forword(find);
        }
    }
};

这是我用双链表实现的一个LRU算法,缓存有两个关系维护数据:一个是key-value之间的映射关系,用map字典之类的;另一个是要维持数据的先后关系,这个先后关系决定了哪些数据会被丢弃,就要类似数组子类的结构,可以直接使用系统库提供的数组,也可以自己用双链表来做。

如果要让这个算法更使用,还需要:

  • 做成模板类,即CacheNode的key和value都是不确定的类型,根据需要指定。
  • 位置1那里,判断内存是否满是靠节点数,但实际应用里肯定是靠数据占有的内存大小而不是缓存的个数,这个判定可以使用一个函数指针(接口、闭包block等类似的东西)把判断逻辑开放出去,由外界根据自身的应用环境来确定。

LFU

这个是Least Frequently Used的缩写,表示最近最不常用。意思上跟LRU好像是一样的,最大的区别是:LRU是使用一次就被提到最前面,而LFU是用一次标记一下,然后按照这个使用次数来排序。换言之,LFU有更精确的统计。

同样是一个数据队列,模拟一下操作过程:

  • 假设尾部是使用次数高的,头部是低的,那么每次都在头部插入数据,因为新数据的使用次数肯定是最少的,丢弃也是在头部。
  • 然后某个数据被使用,它的次数time+1,那么它要往尾部前进。前进到哪里?前进到time+1同级里最前面一个.比如原来是A(10)->B(9)->C(9)->D(8),D被访问后,它的time变成了9,这时它被提到A和B之间,而不是继续在C后面。这是一个会忽视的点。
  • 根据上面一条,新加入的数据,它的time为1,它也应该是加入到同为1的那一级的最前面,而不是在整个队列的头部。这里有细微的差别。A(3)->B(1)->C(1),这时新增一个元素是加在A和B之间,而不是接在C后面。
  • 插入的数已存在,那么也当做一次访问,time+1然后提前。
  • 如果数据满了,但是最后一个节点的time>1,这时还是要淘汰它,不管它的访问次数是几。也就是先淘汰优先级最低的一个,然后再加入数据,再排序

下面是一个算法实现,为了提高效率,使用分层结构:把time相同的节点放在一个队列里,然后用一个大队列把这些队列串起来。因为在数据往前提都是直接越过了time相同的那一层,去到上一层的第一个。如果time相同的数据过多,时间消耗倍增。

class LFUCache {
    struct RowNode;
    struct KeyNode{
        int key;
        int value;
        
        KeyNode *next = nullptr;
        KeyNode *pre = nullptr;
        RowNode *row = nullptr;
        
        KeyNode(int key, int value, RowNode *row):key(key),value(value),row(row){};
        
        bool operator==(const KeyNode &other) const{
            return this->key == other.key;
        }
    };
    
    struct RowNode{
        int time = 0;
        
        RowNode(int time):time(time){};
        
        RowNode *next = nullptr;
        RowNode *pre = nullptr;
        
        KeyNode *keyHead = nullptr;
        KeyNode *keyTail = nullptr;
    };
    
    RowNode *lastRow = nullptr;
    
    int capacity = 0;
    int storedSize = 0;
    unordered_map<int, KeyNode*> store;
    
    void bringForward(KeyNode *node){
        RowNode *preRow = node->row->pre;
        if (preRow == nullptr || preRow->time != node->row->time+1) {
            
            //插入新的行
            preRow = new RowNode(node->row->time+1);
            preRow->next = node->row;
            preRow->pre = node->row->pre;
            
            preRow->next->pre = preRow;
            if (preRow->pre) preRow->pre->next = preRow;
        }
        
        removeKeyNode(node);
        if (node->row->keyHead == nullptr && node->row == lastRow) {
            delete node->row;
            lastRow = preRow;
        }
        insertNodeToFront(node, preRow);
    }
    
    inline void insertNodeToFront(KeyNode *node, RowNode *row){
        if (row->keyHead == node) {
            return;
        }
        
        node->next = row->keyHead;
        node->pre = nullptr;
        
        if (row->keyHead) {
            row->keyHead->pre = node;
        }else{
            row->keyHead = row->keyTail = node;
        }
        
        row->keyHead = node;
        node->row = row;
    }
    
    inline void removeKeyNode(KeyNode *node){
        
        if(node->pre) node->pre->next = node->next;
        if(node->next) node->next->pre = node->pre;
        
        if (node->row->keyHead == node) {
            if (node->row->keyTail == node){
                node->row->keyTail = node->row->keyHead = nullptr;
            }else{
                node->row->keyHead = node->next;
            }
        }else if (node->row->keyTail == node){
            node->row->keyTail = node->pre;
        }
    }
    
public:
    
    LFUCache(int capacity) {
        this->capacity = capacity;
        lastRow = new RowNode(1);
    }
    
    void set(int key, int value) {
        
        if (store.find(key) != store.end()) {
            KeyNode *find = store[key];
            find->value = value;
            bringForward(find);
            return;
        }
        
        KeyNode *newNode = nullptr;
        if (storedSize == capacity) { //满了,解除最后一个,修改值作为新节点
            newNode = lastRow->keyTail;
            removeKeyNode(lastRow->keyTail);
            
            store.erase(newNode->key);
            newNode->key = key;
            newNode->value = value;
            
            if (lastRow->keyHead == nullptr) {
                lastRow->time = 1;
            }
        }else{
            newNode = new KeyNode(key, value, lastRow);
            storedSize++;
        }
        
        if (lastRow->time != 1) {
            auto newLast = new RowNode(1);
            newLast->pre = lastRow;
            lastRow->next = newLast;
            
            lastRow = newLast;
        }
        
        store[key] = newNode;
        insertNodeToFront(newNode, lastRow);
    }
    
    int get(int key) {
        if (store.find(key) == store.end()) {
            return -1;
        }
        
        KeyNode *find = store[key];
        bringForward(find);
        return find->value;
    }
};

最后这里是我的算法练习库

相关文章

网友评论

    本文标题:缓存策略之LRU和LFU

    本文链接:https://www.haomeiwen.com/subject/lgpugftx.html