LRU和LUF的设计与实现

256 阅读5分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第二篇文章,点击查看活动详情

最近两天刚好在leetcode上写了146.LRU缓存和460.LFU缓存,稍微的来总结下。

首先看题目要求

设计实现 一个LRUCache 类:

1.LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存

2.int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -13.void put(int key, int value) 如果关键字 key 已经存在,
则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。
如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

4.函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

对于O(1)的时间复杂度可以使用哈希表来实现对 key-value 结构的储存,这样对于查找操作时间复杂度就是 O(1)。 而对于put操作,首先要考虑的是如何能够得到最久未使用的key-value,所以使用了链表来做存储,每次使用key-value 时将key对应的链表节点移动到链表的开头,则最后在链表尾的则为最久未使用的关键字,可在移出最尾节点时,单链表需要从头开始遍历(无法满足题目的O(1)条件),所以使用双链表来作为储存。

struct node{
    int key ,value;//key-value
    node* next;
    node* prev;
    node():key(0),value(0),prev(nullptr),next(nullptr){}
    node(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){}
};

class LRUCache {
private:
    unordered_map<int,node*>hash;
    node*head;//使用假头节点和尾节点,方便对节点的移动和删除
    node* tail;
    int size=0;
    int c=0;

public:
    LRUCache(int capacity):c(capacity),size(0) {
    head=new node();
    tail=new node();
    //建立空的双链表
    head->next=tail;
    tail->prev=head;
    }
    
    int get(int key) {
        if(!hash.count(key))return -1;
        node *num=hash[key];
        move(num);
        return num->value;
    }
    
    void put(int key, int value) {
        if(hash.count(key))//如果key存在
        {
            hash[key]->value=value;
            node *num=hash[key];
            move(num);
            return ;
        }
        node*num=new node(key,value);
        hash[key]=num;
        addToHead(num);
        ++size;
        if(size>c){
            node* remove=removeTail();
            hash.erase(remove->key);
            delete remove;
            --size;
        }
    }
    
    void addToHead(node *num)//将节点添加到假头后
    {
        num->prev=head;
        num->next=head->next;
        head->next->prev=num;
        head->next=num;
    }

    void removenode(node *num)//将节点移出链表
    {
        num->prev->next=num->next;
        num->next->prev=num->prev;
    }

    void move(node *num)//将调用过的key对于节点放在假头节点后
    {
        removenode(num);
        addToHead(num);
    }
  

    node* removeTail()//移除最久未使用的节点,并返回进行删除
    {
        node*num=tail->prev;
        removenode(num);
        return num;
    }
};


再来看460.LFU缓存题 ,与LRU不同的是LFU需要删除的是使用次数最少的key-value结构,且使用次数相同的时候,删除LRU规则最久未使用的key-value。

所以使用多一个双链表来维护技术器,链表从头节点开始到尾节点的ide(使用次数)成单调增加的形势,因为为了节省空间,所以某使用次数的节点为空时则进行删除,而在计算节点里面,都维护着一个与LRU相同的双链表来得到最久未使用key。这样当容量过载时,需要删除的是距离假头节点最近的节点里的双链表的假尾节点前的节点。

首先来看,什么时候会进行删除空节点,一个是移动节点时,一个则是在删除节点时,则需要在这两个情况的时候进行对他们之前所在的节点进行是否为空的判断;

struct Node{
    int idx;
    int key;
    int value;
    Node *next;
    Node *prev;
};//跟LUR相同的节点
struct tong{
    int idx;
    Node *head;
    Node *wei;
    tong *next;
    tong *prev;
    tong():head(new Node()),wei(new Node()){
        head->next=wei;
        wei->prev=head;
    }
};//计数的桶节点,里面维持着LUR的双链表
class LFUCache {
private:
    int capacity;
    int time;
    tong *head;
    tong *wei;
    unordered_map<int,Node*>hashkey;//key-node*
    unordered_map<int,tong*>hashidx;//idx-tong*

public:
    LFUCache(int capacity1):capacity(capacity1),time(0) {
        head=new tong();
        wei=new tong();
        //建立桶的双端链表
        head->next=wei;
        wei->prev=head;
    }
    
    int get(int key) {
    if(!hashkey.count(key))return -1;
    Node *node=hashkey[key];
    node->idx++;
    move(node);
    return hashkey[key]->value;   
    }
    
    void put(int key, int value) {
        if(capacity==0)return;
        if(hashkey.count(key))
        {
              Node *node=hashkey[key];
              node->value=value;
              node->idx++;
              move(node);
              return;
        }
         ++time;
        if(time>capacity)//先进行节点的删除再添加新节点,否则当使用idx=1的节点里只有新节点时会导致删除新节点
        {
            Node *node=movewei(head->next);
            hashkey.erase(node->key);
            delete node;
            time--;
        }
        Node *node=new Node();
        node->key=key;
        node->value=value;
        node->idx=1;
        hashkey[key]=node;
        if(hashidx.count(1))addlist(node,hashidx[1]);//判断idx=1的桶是否还在
        else{
            tong *node1=new tong();
            node1->idx=1;
            node1->next=head->next;
            node1->prev=head;
            head->next->prev=node1;
            head->next=node1;
            hashidx[1]=node1;
            addlist(node,hashidx[1]);
        }
        return ;
    }
    
    void movenode(Node *node)//将节点从链表中移出
    {
        node->next->prev=node->prev;
        node->prev->next=node->next;
    }
    void move(Node *node)
   {
        if(!hashidx.count(node->idx))//判断有无node->idx这个桶
        {
            tong *node1=new tong();
            node1->idx=node->idx;
            tong *node2=hashidx[node->idx-1];
            node1->next=node2->next;
            node1->next->prev=node1;
            node2->next=node1;
            node1->prev=node2;
            hashidx[node->idx]=node1;
        }
        tong *node1=hashidx[node->idx];
        tong *node2=hashidx[node->idx-1];
        movenode(node);
        if(node2->head->next==node2->wei)//判断移去node后,之前的桶是否为空
        {
            hashidx.erase(node2->idx);
            shanlist(node2);
        }
        addlist(node,node1);
    }
    void addlist(Node *node,tong *node1)//将node放入node1的桶内
    {
        Node *head=node1->head;
        node->next=head->next;
        node->prev=head;
        head->next=node;
        node->next->prev=node;
    }
    
    Node* movewei(tong *node1){
        Node *node=node1->wei->prev;
        movenode(node);
        if(node1->head->next==node1->wei)//判断移去node后,之前的桶是否为空
        {
            hashidx.erase(node1->idx);
            shanlist(node1);
        }
        return node;
    }
    void shanlist(tong *node1)//移除空桶并删除
    {
        node1->next->prev=node1->prev;
        node1->prev->next=node1->next;
        delete node1;
    }
};