LRU与LFU缓存实现(c++)

背景

进程通过malloc函数申请内存时分配到的内存是虚拟内存,并不是实际的物理内存,且此时内存管理单元没有完成虚拟内存到物理内存的映射。当程序需要访问虚拟内存时,发现该内存并没有映射到物理内存上,会产生缺页中断。进入缺页中断后判断是否有可分配的物理内存,如果有,这时才会分配物理页并通过MMU的页表来建立虚拟内存到物理内存的映射。

如果没有可分配的物理内存,则会按顺序进行后台内存回收直接内存回收,如果进行内存回收后仍无法满足需求,则会出发OOM机制,杀死占用内存较大的进程。

若缺页中断后发现物理内存中没有可分配的空闲页,则需要使用页面置换算法选择物理页(脏页)置环到磁盘,本文主要介绍其中LRU、LFU。

LRU算法选择最长时间没被访问的内存页进行置环,一般通过维护链表来实现,链表头部区域是经常访问的内存,尾部区域是不常访问的内存,也就是优先回收链表尾部的内存。LFU算法选择访问次数最少的页面进行置换若访问次数相同,则对相同次数的页面使用LRU算法来进一步判断置换掉哪一页

LRU

题目

在这里插入图片描述

数据结构

LRU算法保证在每次访问元素时,需将该元素插入到链表头部,若当前容量大于维护的容量时,需要从尾部删除元素,链表的插入删除很方便,时间复杂度满足要求;但是查找元素时需要遍历,这时可考虑用哈希表映射,实现查询也满足O(1)的时间复杂度。
如下图所示:哈希表里存放key–>node的映射关系,双向链表里也含有key的原因是因为后续删除链表时,需要同时删除哈希表里的对应元素。
在这里插入图片描述

具体实现

双向链表结点的定义

四个元素:key、value以及前驱节点与后继结点

class DoubleList{
public:
    int key;
    int value;
    DoubleList *prev;
    DoubleList *next;
    DoubleList():key(0), value(0), prev(nullptr), next(nullptr){}
    DoubleList(int _key, int _value):key(_key), value(_value), prev(nullptr), next(nullptr){}
};

LRUCache类

初始化

定义初始化的成员变量:头尾结点、哈希表、容量、当前大小

class LRUCache {
public:
    LRUCache(int capacity) : capacity_(capacity), size_(0){				//数据结构初始化
        head = new DoubleList();
        tail = new DoubleList();
        head->next = tail;
        tail->prev = head;
        //*******************
        //
        //*******************
    }
private:
    DoubleList *head;
    DoubleList *tail;
    unordered_map<int, DoubleList *> map;
    int size_;
    int capacity_;  
};
主要接口实现

put()
若没有该元素,则new出该元素结点,将其加入链表头部,在哈希表里也加入映射关系,因为新加入元素,需要判断大小是否超出LRUCache的容量,若超出,删除尾结点以及对应的哈希表映射。若有,则更新value,并加入链表头部。

void put(int key, int value) {
    if (map.find(key) == map.end()) {
        DoubleList *node = new DoubleList(key, value);
        //map[key] = node;
        map.insert(pair<int, DoubleList*>(key, node));
        addHead(node);
        size_++;
        if (size_ > capacity_) {
            DoubleList *tmp = tail->prev;
            removeNode(tmp);
            map.erase(tmp->key); //需要删除哈希表,所以node里也有key
            delete(tmp);
            size_--;
        }
    }
    else {
        DoubleList *node = map[key];
        node->value = value;
        moveToHead(node);
    }
}

get()
若元素存在,则返回该值,并将元素放入链表头节点

 int get(int key) {
     if (map.find(key) == map.end()) {
         return -1;
     }
     else {
         DoubleList *node = map[key];
         moveToHead(node);
         return node->value;
     }
 }

addHead()removeNode()moveToHead()等与双链表相关的操作就不赘述了…
这里与操作链表相关的函数写在了LRUCache类中,其实应该定义一个结点类和一个双向链表的类,将链表相关操作封装在链表类里。后面LFU会写规范一点。

LFU

题目

在这里插入图片描述

数据结构

特点:

  • FreqDoubleList链表存放的是按访问次数从小到大排序的结点
  • DoubleList里的是真正的存放数据的结点
  • FreqDoubleList中的每一个freqNode结点下面都有自己的DoubleList
  • freq_map存放访问的key存储访问该key的次数的结点freqNode的映射;
  • lru_map与前述LRU算法中的map一样,不同的是这里可以映射到多个链表的结点。每个freqNode下面都有一个LRU双链表,因为访问次数相同的话,要根据LRU算法再进一步决策。

在这里插入图片描述

具体实现

结点类与链表类的定义

  • LRUDoubleList链表与其Node 的定义:

在链表类中实现插入删除结点的接口,与前面类似。不同之处在于isempty(),该函数表示判断此LRUDoubleList是否没存数据,因为该链表是属于freqNode的,若进行操作后没有访问次数等于freqNode->fre_的数据,则ferqNode要删除,所以要判断该链表是否为空,进而对没用的freqNode结点删除。

class Node {
public:
    int key_;
    int value_;
    Node *prev;
    Node *next;
    Node():key_(0), value_(0), prev(nullptr), next(nullptr){}
    Node(int key, int value):key_(key), value_(value), prev(nullptr), next(nullptr){}
};

class LRUDoubleList {
public:
    Node *head;
    Node *tail;
    LRUDoubleList():head(new Node()), tail(new Node()) {
        head->next = tail;
        tail->prev = head;
    }

    void addHead(Node *node) {
        node->next = head->next;
        node->prev = head;
        head->next->prev = node;
        head->next = node;
    }

    Node* delNode(Node *node){
        node->next->prev = node->prev;
        node->prev->next = node->next;        
        return node;
    }

    Node* delLast() {
        return delNode(tail->prev);
    } 

    bool isempty() {
        return head->next == tail;
    }

};
  • FreDoubleList链表与其freqNode 的定义:

比较重要的地方在FreDoubleList头尾节点赋值addCurFreq()函数

class FreqNode {
public:
  int freq_;
  FreqNode *prev;
  FreqNode *next;
  //*****************一个freqNode结点对应一个LRU链表*****************
  LRUDoubleList *dl; 
  
  //explicit防止隐式转换,其实就是想稍微规范一点所以写成这样
  explicit FreqNode(int freq):freq_(freq), prev(nullptr), next(nullptr), dl(new LRUDoubleList()){}
  
};

class FreDoubleList {
public:
  FreqNode *head;
  FreqNode *tail;
  
  // 初始化freqDoubleList时头尾节点可设置为 0 与 INT_MAX
  FreDoubleList():head(new FreqNode(0)), tail(new FreqNode(INT_MAX)) {
      head->next = tail;
      tail->prev = head;
  }

  //当前频率次数加一
  void addCurFreq(FreqNode *cur) {
      //if not exist, create new freq;
      if (cur->freq_ + 1 != cur->next->freq_) {
          FreqNode *newFreqNode = new FreqNode(cur->freq_ + 1);

          newFreqNode->next = cur->next;
          newFreqNode->prev = cur;
          cur->next->prev = newFreqNode;
          cur->next = newFreqNode;
      }
  }

  void delFreqNode(FreqNode *node) {
      node->next->prev = node->prev;
      node->prev->next = node->next;
  }
};

LFUCache类

成员变量定义

前文所述需要的两个散列表;表示访问次数的链表,还有其下的LRU链表(创建freqNode时创建);存放数据的容量

private:
    //int size_;
    int capacity_;
    unordered_map<int, FreqNode*> fre_map;
    unordered_map<int, Node*> lru_map;
    FreDoubleList *fl;
接口实现

get()
没找到直接返回-1;
若找到,先删除原有的结点,然后更新数据结构

int get(int key) {
	//没找到该数据,直接返回-1
    if (lru_map.find(key) == lru_map.end()) {
        return -1;
    }
 	//数据存在,进行两个步骤:1。删除原有的;2.更新数据结构
    //1 、delete old
    //访问次数+1,故先增加对应的freqNode结点
    FreqNode *before = fre_map[key];
    fl->addCurFreq(before);
    
    //访问了,所以fre + 1,即before->next就是刚增加的ferqNode结点
    FreqNode* after = before->next;     
    Node *targetNode = lru_map[key];
    //删除原先的freqNode下的LRU链表中的结点
    before->dl->delNode(targetNode);
    //判断该LRU链表还是否有元素,没有则阐述freqNode结点
    if (before->dl->isempty()) {
        fl->delFreqNode(before);
    }
    //2、update to the next freq node
    after->dl->addHead(targetNode);  //更新frqNode下的LRU链表里的结点
    fre_map[key] = after;           //访问次数改变了,需要更新映射
    return targetNode->value_;
   
}

put()

void put(int key, int value) {
    if (capacity_ == 0) return;
    if (lru_map.find(key) != lru_map.end()) {
        lru_map[key]->value_ = value;
        get(key);
    } 
    else {			//若没有元素,则需要添加
        if(lru_map.size() == capacity_) {
            //if will out of size, shoule delete node(Last LruNode in first LRUDoubledList)
            FreqNode *node = fl->head->next;
            Node *nodeToDel = node->dl->delLast();
            //delete map
            lru_map.erase(nodeToDel->key_);
            fre_map.erase(nodeToDel->key_); 
            //最少频率列没有元素了,删除  ....
            if (node->dl->isempty()) {
                fl->delFreqNode(node);
            }
        }
    
        //add node must update fl->head
        fl->addCurFreq(f1->head);
        Node *newNode = new Node(key, value);
        fre_map[key] = fl->head->next;
        lru_map[key] = newNode;
        //************注意这里的结构嵌套****************
        fl->head->next->dl->addHead(newNode);
    }
}

小结

LFU的编写更加规范,将链表与其结点分别定义,结构较清晰。今天的一天都在写LRU与LFU。。。溜了写基金去了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值