【自适应缓存替换(ARC)算法】讲解及c++实现

写在前面

自适应缓存替换算法(ARC)算法是缓存淘汰算法里面的一种,是结合了LRU(Least Recently Used, 最近最少使用, 或者说最长时间未使用)淘汰算法和LFU(Least Frequently Used, 最近最少使用)淘汰算法两种算法优点的一种性能较为优越的算法,通过两个表的内存空间动态调整来实现内存更有效的使用

这一次的文章也是我的一次数据结构实验大作业(QwQ)

目前在C站里面,详细讲解ARC缓存淘汰算法的文章并不很多, 而且大多只是讲个大概,很少有比较完整的实现代码, 所以就自己做了一下进行实现。

对于ARC,LRU,LFU缓存淘汰算法,都是利用双向链表和哈希表的一种缓存 在代码补全和输入法的常用字缓存机制中会有广泛的使用。

比较好的参考文章放在前面:
LRU缓存算法参考
针对ARC算法过程,我主要参考了下面这一篇文章:
ARC算法过程参考文章

另外,由于笔者水平所限,文章中可能有的地方叙述不够准确,或者有的细节和实际ARC算法有出入,如果有不够准确的部分,还请读者在评论区指正。 另外这个程序没有太多经过刁钻的测试,在稳定性和性能优化上都有不足,如果在实际使用中需要,项目github地址和源代码都在代码部分,可以自行阅读修改

程序运行环境:visual studio 2019 (需要c++11以上运行环境),文章讲解使用的绘图软件:draw.io

一、简介

ARC算法(Adaptive Replacement Cache,自适应缓存替换算法)是美国IBM Almaden 研究中心开发的一种缓存算法,这种算法结合了LRU和LFU的各自的优点,可实现高效的缓存。

在中文输入法和各种IDE的代码补全功能中,为了保证代码补全功能实现,每一次需要将最近使用频繁的元素存放在缓存中,然后在用户输入时,从缓存中搜索最近使用过的单词然后提供给用户进行补全,而中文输入法也是往往会将使用频率高的词进行前置供用户输入。

本程序使用ARC算法实现代码自动补全类型的缓存结构进行缓存,实现类似于输入法和IDE代码补全中的缓存功能。而ARC算法是一种结合LRU和LFU缓存算法的算法,所以这里先解释LRU和LFU算法的原理。

我们以词汇的缓存为例分别说明LRU, LFU和ARC算法。首先,LRU算法是一种仅存储最近使用过词语的一种存储结构,其原理是利用链表的有序性,在使用词汇时,如果使用的词汇在缓存中,则对最近使用的词汇进行前置,而每一次缓存区满时,我们会删除最长时间没有使用过的节点
(如下图中,python被删除),并将新词语插入到链表的头部。
在这里插入图片描述
而对于LFU算法,则是在节点中维护一个关键词time, 存储对应的词汇被调用的次数。访问次数越大的,则越靠前,每一次加入新词汇时,将新词汇有序地插入链表的合适位置,如果表已经满,则删除其中访问次数最少的元素,如果其中有多个访问次数最少的元素,则删除其中距离现在调用时间最长的元素 (这里利用了链表的有序插入特性,每次需要在链表插入时,将新的节点插入到第一个使用次数为1的位置, 而越靠近头结点,使用次数越大,越靠近尾结点,使用次数小且更长时间未使用)

如下图中插入halo时, 应当设置halo次数为1并插入到ambient前方, 这样就保证了后方淘汰掉的是长时间未高频使用过的元素

LFU基本方法如图所示:
在这里插入图片描述

二、 存储和访问原理

(1) 哈希链表的存储结构

不论LRU还是LFU算法,为了达到高效的缓存和查找效率,其存储结构是使用哈希链表进行的。

哈希链表的原理是在初始化时添加一个value为指针类型的哈希表,初始化时,每一次将添加链表节点到哈希表中,并以链表的key作为键值,如下图所示。
在这里插入图片描述
要实现一个简单的哈希链表,只需要定义一个哈希表和一个链表就行了

#include <iostream>
using namespace std;
typedef string DataType;
// 缓存节点链表
typedef struct CacheList {
    
    
    DataType key;  // 值是string
    int time = 0;  // 定义使用计数器,计算使用次数
    CacheList* prev = nullptr;
    CacheList* next = nullptr;
};
CacheList* cache = nullptr;
unordered_map<DataType, CacheList*> map;

// 然后怎么建立链表我就不说了,只需要添加节点时加入map[data] = p就行了

对于哈希链表式的存储结构,结合了哈希表查找快和链表的有序特点,可以使用哈希表存储链表的节点,实现时间复杂度为O(1)的查找速度。使用哈希链表是缓存中一种很高效的存储办法。

而在下面的程序实现中,在LRU的插入,查找和删除的复杂度为O(1),而在LFU部分由于是有序插入,插入时间复杂度为O(n),而查找和删除的复杂度为O(1), 但是对于LFU本身其实可以通过双哈希表的方法将其插入复杂度降低到O(1)的,时间原因我没有进行实现, 具体可以参考力扣第460题

2. LFU和LRU的缺点分析

LRU缓存淘汰算法和LFU缓存淘汰算法都是有其缺陷的,对于LRU算法,由于仅记录访问元素的顺序,对于我们常用的高频词组难以保存。尤其是当在短时间内大量访问新数据的情况,往往会将我们常用的高频词语驱逐出缓存,而导致补全效果下降。而对于LFU算法,如果某些生僻的词语在短时间内受到大量使用,则之后很难将其逐出缓存,导致缓存中几乎无法盛放最近使用的词语。因此上述两种缓存淘汰算法均会产生缓存利用不充分的现象,即缓存污染现象。

3. ARC算法的基本思想

ARC算法的基本思想是, 结合LRU缓存方法和LFU缓存方法,为了能够充分利用缓存空间,我们可以将一个固定大小的缓存区分为LRU缓存区和LFU缓存区两个部分,如下图所示。
在这里插入图片描述
ARC算法的动态分配内存的思想是,在输入重复数据较多时,LFU区域长度增加,存储更多的高频词汇内容;而在多次输入不同的数据时,LRU内存区域增加,存储最近使用内容增多。这样就可以实现内存的动态分配了。

在上图的图示(ARC原先设计思路)中,Partition为分隔指针,为了能够实现内存的充分使用,Partition指针的位置可以进行动态调整,而在文末的的程序中,我没有使用partition指针, 而是使用两个长度之和为定值的双链表分别进行存储的

其中,较为重要的是如何实现上述两个表的大小动态调整。为了判断目前的输入方法更倾向于输入不同的数据还是频繁输入相同数据,我们可以对LRU区域和LFU区域分别建立一个淘汰链表(或者称为虚表,即ghost链表),分别存储LRU和LFU中被淘汰的元素。这两个链表分别称为LRU_ghost 和 LFU_ghost,在实际使用中为了节省内存,ghost表部分只存储键值,而不存储使用次数。由于本程序注重缓存功能实现(其实是我懒),在放入ghost表时,将次数直接进行置零操作(这会浪费存储空间)。综合以上的叙述,ARC缓存的存储结构是通过四个哈希链表完成的。绘图如下:
在这里插入图片描述
在输入过程中,对于某个部分存储满时,如果继续加入元素,则LRU或者LFU按其对应的算法逐出该表尾部的元素并插入对应的ghost表中
对于在LRU_ghost中的数据,储存在表内且在访问次数达到一定数量(transform time)时,数据会加入到LFU表中,即加入到经常访问的元素中
在这里插入图片描述

实现缓存动态分配的方法
每一次在输入数据时,先检查两个ghost cache中是否有对应的数据。如果数据在LRU_ghost中,说明目前访问元素的方法更接近于访问陌生元素。 此时就会扩大LRU区域的空间并将这个元素放入LRU表内。而如果数据在LFU_ghost中出现,说明目前访问方法更多频繁访问某些元素,此时,我们让LFU区域的空间增加而让LRU区域减小。然后, 我们在LFU_ghost中查找是否有对应的元素,如果有则说明更趋向于访问频繁访问的元素, 如果在LFU_ghost中找到,则让LFU空间减小而让LRU空间增大,这样就实现了动态分配有限的缓存空间。

三、程序基本结构

(这一段直接从我报告里复制的)
为了能够实现ARC缓存的功能, 定义两个类LRUcache和LFUcache,由于每一个各自有一个缓存表空间cache和一个ghost链表空间,分别在两个类中各自定义cache_map和ghost_map,用来分别使用哈希表存储索引,进行高速搜索。另外对应有cache对应的size和ghost_size来检查表是否满或者未满进行动态删除元素。在判断表满时,size对应的容量是capacity,而ghost_size对应的容量是ghost_capacity。

然后在大的类ARCcache里面各包含一个LRUcache和一个LFUcache,我们只需要在ARCcache的类中,让两个链表存储空间的总和相同即可,因此我们需要在LRUcache和LFUcache中分别实现如下的算法

在添加元素时,首先搜索在ghost中是否出现,如果出现则调用其中一表的扩容和另一表的缩减函数, 如果没有则调用put函数

  1. 向链表中添加元素的put函数,在表满情况下,则弹出(淘汰)表尾的元素并放入cache中,其中在向ghost添加元素时,如果ghost已满,则自动弹出尾部的元素。
  2. 在LRU链表中元素使用达到一定次数时,需要返回一个值,提示put函数加入一个相同的元素到LFU链表中。
  3. 扩容函数: 在表中插入元素同时判断目前是否能够扩容,如果能则让表容量+1
  4. 缩减函数: 让表容量-1,如果表满的话需要另外移除表尾的元素。
    因此分别LRUcache和LFUcache类本身,需要定义的函数有put(),即在该部分插入元素。除此以外,还需要分别定义令其改变空间的函数Add()和Subtract(),分别对应加上一个元素空间和减去一个元素的空间。

需要说明的是,上述几个函数是作为LRU和LFU类的调用函数(即ARC类直接调用LRU和LFU的上述函数), 而在LFU和LRU类中,还分别定义了如下的函数供put,Add和subtract调用。

  1. Insert : 直接插入一个函数在链表中(根据is_ghost判断是向原始链表中插入还是在ghost表中插入) , 其中对于LRU算法,是直接在头部插入的(保证时间顺序),而对于LFU算法,会进行一次在表中的扫描并插入到次数time合适的位置(上面讲过)
  2. DeleteTail: 删除链表的尾部元素, 如果不是ghost链表,则删除之后自动调用Insert函数把删的节点插入ghost链表中
  3. DetachNode: 对于重复访问缓存中元素的情况, 需要变更数据在缓存中的位置, 此时需要将节点暂时从链表中移出,但是不释放指针空间以便调用Insert之后插入(对于LRU是头部,对于LFU是合适位置)。不过ghost链表暂时删除元素时,会删除ghost的哈希表键值对以便向主表中插入
  4. Show() 显示缓存区内容,方便调试(

四、代码部分

程序使用方法是: 每一次输入一个词语按下回车,程序会自动统计词频并输出缓存的情况(附注:把main函数第12行的true改为false可以不输出ghost的缓存内容)

这个程序一共有5个源程序, 所以我放在了github上, 可以通过我的github项目地址找到

如果不放便访问github也没关系,下面贴上全套源文件和代码:
这个代码把类的实现和函数定义分别放到了不同的位置, 比较有封装性:

main.cpp

#include "ARCcacheHeader.h"

// 使用要求: 可以一次输入多个词组,并将每一个词组放置在缓存中,
int main()
{
    
    
    ARCcache container(5, 3); 
    // 注意: 这个是生成一个大小为10 (5x2, LRU和LFU初始尺寸均为5)的缓存
    // 并且transform_time为3, 即输入3次相同单词,会帮你加入last frequent used 表中
    DataType input; 
    while(true) {
    
    
        getline(cin, input);
        container.put(input);
        if (input == "exit") break;
        container.Show(true);  // 改为false时,不显示ghost表中的内容
    }
    return 0;
}

/* TestCode : 
ARCcache container(5, 3);
container.put("hello");
container.put("hella");
container.put("hellb");
container.put("world");
container.put("hellc");
container.put("hellk");
container.put("world");
container.put("world");
container.put("world");
container.put("hellb");
container.put("hellt");
container.Show(true);
container.put("hella");
container.Show(true);
*/

ARCcacheHeader.h

#pragma once 
#include <iostream>
#include <string>
#include <unordered_map>  // 使用c++自带的哈希表
#ifndef ARCCACHEHEADER
#define ARCCACHEHEADER   // 避免头文件重复定义
#define Init_Capacity 30
#define Init_Transfrom_Time 3

using namespace std;
typedef string DataType;
/*简介:
* 使用LFU和LRU结合的方法(也即A),实现对词汇进行自动补全功能中的缓存列表的存储操作
* 在本程序中,使用哈希表和双向链表的存储结构,结合了链表的有序特性和哈希表的查找特性
* 双向循环链表是便于移除尾部的元素
* 基于LFU(Least Recently User,最近不常用算法)和LRU(最近最少使用算法)实现一固定长度链表的插入
* 使用函数get和put来进行词组的读入操作, 并且要求get和put需要以O(1)的时间复杂度运行
*/

// 缓存节点列表
typedef struct CacheList {
    
    
    DataType key;  
    int time = 0;  // 定义使用计数器,计算使用次数
    CacheList* prev = nullptr;
    CacheList* next = nullptr;
};

// LRU类部分
class LRUcache {
    
    
public:
    LRUcache() {
    
    
        this->capacity = Init_Capacity;
        this->ghost_capacity = Init_Capacity;
    }
    LRUcache(int capacity) {
    
    
        // 注意这里不够健壮,这是由于初始化的capacity可能小于0,但暂且不管健壮性
        this->capacity = capacity;
        this->ghost_capacity = capacity; // 对ghost分配同样的存储空间
        // 分配同样大小的空间
    }
    LRUcache(int capacity, int transform_time) {
    
    
        // 注意这里不够健壮,这是由于初始化的capacity可能小于0,但暂且不管健壮性
        this->capacity = capacity;
        this->ghost_capacity = capacity; // 对ghost分配同样的存储空间
        this->transform_time = transform_time;
        // 分配同样大小的空间
    }
    bool put(DataType data); // 输入元素
    void Add(DataType data); // 输入并且扩充一格
    bool Subtract();         // 删除末尾元素且空间缩减,返回是否成功
    // 其中,由于只是插入元素,则对于ghost的处理放在插入元素函数中即可
    bool check_ghost(DataType data); // 检查ghost中是否有对应元素,有则返回true
    void Show(bool show_ghost); // 显示所有元素

private:
    CacheList* cache = nullptr;
    unordered_map<DataType, CacheList*> map;
    int size = 0;
    int capacity = Init_Capacity;

    CacheList* ghost = nullptr;
    unordered_map<DataType, CacheList*> ghost_map;
    int ghost_size = 0;
    int ghost_capacity = Init_Capacity; // ghost链表的存储空间(固定不变的)

    void Insert(CacheList* L, bool is_ghost); // 直接插入节点
    void DeleteTail(bool is_ghost); // 删除末尾节点
    void DetachNode(CacheList* L, bool is_ghost); // 脱离节点,即从表中删除但是不释放空间
    int transform_time = Init_Transfrom_Time; // 默认从LRU到LFU转移阈值是3

    /*
    * 插入函数的要求: 如果size < capacity,直接插入
    * 如果capacity已满,则先删除末尾的元素并将其加入ghost(单独定义加ghost函数)
    * 然后再将元素插入本身中
    *
    * 此时调用加入ghost的函数,这个函数的结构和Insert相同(直接插头结点)
    * 然后ghost如果满了则删除最后一个元素
    */
};

// LFU类部分
class LFUcache {
    
    
public:
    LFUcache() {
    
    
        this->capacity = Init_Capacity;
        this->ghost_capacity = Init_Capacity;
    }
    LFUcache(int capacity) {
    
    
        // 注意这里不够健壮,这是由于初始化的capacity可能小于0,但暂且不管健壮性s
        this->capacity = capacity;
        this->ghost_capacity = capacity;
        // 分配同样大小的空间
    }
    LFUcache(int capacity, int transform_time) {
    
    
        // 注意这里不够健壮,这是由于初始化的capacity可能小于0,但暂且不管健壮性s
        this->capacity = capacity;
        this->ghost_capacity = capacity;
        this->transform_time = transform_time;
        // 分配同样大小的空间
    }
    void put(DataType data); // 向LFU内增加缓存元素
    void Add(DataType data); 
    bool Subtract();
    bool check_ghost(DataType data); // 检查ghost中是否有对应元素,有则返回true
    void Show(bool show_ghost);
private:
    CacheList* cache = nullptr;
    unordered_map<DataType, CacheList*> map;
    int size = 0;
    int capacity = Init_Capacity;
    
    int transform_time = Init_Transfrom_Time; // 需要和LRU内一致
    
    CacheList* ghost = nullptr;
    unordered_map<DataType, CacheList*> ghost_map;
    int ghost_size = 0;
    int ghost_capacity = Init_Capacity;
    
    void DetachNode(CacheList* L, bool is_ghost);
    void DeleteTail(bool is_ghost); // 删除尾部元素
    void Insert(CacheList* L, bool is_ghost);     // 有序插入元素
};

class ARCcache {
    
    
public:
    ARCcache() {
    
    
        this->capacity = Init_Capacity;
        this->Rcache = new LRUcache(Init_Capacity);
        this->Fcache = new LFUcache(Init_Capacity);
    }
    ARCcache(int capacity) {
    
    
        this->Rcache = new LRUcache(capacity);
        this->Fcache = new LFUcache(capacity);
        this->capacity = capacity;
    }
    ARCcache(int capacity, int transform_time) {
    
    
        this->Rcache = new LRUcache(capacity,transform_time);
        this->Fcache = new LFUcache(capacity,transform_time);
        this->capacity = capacity;
        this->transform_time = transform_time;
    }
    void put(DataType data);
    void Show(bool show_ghost);
private: 
    // 一共有两个缓存指针,  分别存储Rcache和Fcache
    LRUcache* Rcache = nullptr;
    LFUcache* Fcache = nullptr;
    int size = 0;
    int capacity = Init_Capacity;
    int transform_time = Init_Transfrom_Time;  // 初始化为初始转变次数
    // 初始转变次数是触发消息频次达到几次时, 就加入LFU的cache
    // 默认加入的是LRU的cache,但是当触发达到一定次数,就会加入LFU的cache
};


#endif // !ARCCACHEHEADER

LRUfunc.cpp

#include "ARCcacheHeader.h"

// put 是直接向缓存空间输入的函数: 如果次数大于3返回true, 否则返回false
bool LRUcache::put(DataType data) {
    
    
    // 对于另一个把这个空间全部占用掉的情况,此时capacity=0,不插入
    if (this->capacity == 0) return false;

    if (map.find(data)!= map.end()) {
    
     // 如果找到, 则将节点移到头部, 并将
        CacheList* L = map[data]; // 注意: 如果map[data]->time > 3, 则需要添加到LFUcache中
        L->time += 1;
        // 删除之后,在头部添加相应的节点
        DetachNode(L, false); // 从节点中移除
        Insert(L, false);     // 把这个节点放在头结点(第一个访问的节点)
        if (L->time >= transform_time) return true;  // 在LFU中加入一次
    }
    else {
    
     // map中没有找到
        CacheList* L = new CacheList(); // 新建缓存节点
        map[data] = L;  // 将节点存储在哈希表中
        if (this->size == this->capacity) {
    
    
            DeleteTail(false);
            // 删除节点,探后放进ghost中
        }
        L->key = data;
        L->time = 1;    // 使用次数为1
        Insert(L, false); // 在缓存中插入节点
    }
    return false;
}

// 使用头插方法,仅插入节点, 不考虑删除
void LRUcache::Insert(CacheList *L, bool is_ghost) {
    
    
    if (!L) return;
    if (is_ghost) {
    
    
        if (!this->ghost) {
    
    
            ghost = L;
            ghost->next = ghost;
            ghost->prev = ghost;
        }
        else {
    
    
            CacheList* q = ghost->prev;
            L->next = ghost;
            L->prev = q;
            q->next = L; 
            ghost->prev = L;
            this->ghost = L;
        }
        this->ghost_size += 1;
    }
    else {
    
     // 向主表内插入
        if (!cache) {
    
     // 对于头结点为空的情况
            cache = L;
            cache->next = cache;
            cache->prev = cache;
        }
        else {
    
    // 由于是头结点访问时间最近,所以使用头插方法, 插入完之后重新给head节点赋值
            CacheList* q = cache->prev;
            L->next = cache;
            L->prev = q;
            cache->prev = L;
            q->next = L;
            this->cache = L;
        }
        this->size += 1;
        // 插入完毕之后,头结点是最近访问的元素
    }
};

// 对于主List是让L暂时取出链表不释放指针(已经置空前后指针),之后调用插入函数进行头插
// 而对于ghost_list是删除哈希表元素,不释放指针便于后续往主List中插入
void LRUcache::DetachNode(CacheList* L, bool is_ghost) {
    
    
    // 注意需要判断是否为ghost,虽然代码相似,但是一个是size-1, 另一个ghost_size-1
    if (!is_ghost) {
    
     // 暂时移除指针,不抹除哈希表中的元素
        if (this->size == 0) {
    
    
            throw runtime_error("nothing to detach!");
        }
        if (this->size == 1) {
    
    
            this -> cache = nullptr;
        }
        else if (L == cache) {
    
     // 暂时清除头结点
            this -> cache = L->next;
            cache->prev = L->prev;
            L->prev->next = cache;
        }
        else {
    
     // 取出中间节点
            CacheList* q = L->prev;
            q->next = L->next;
            L->next->prev = q;
        }
        L->prev = nullptr; L->next = nullptr;
        size -= 1;
    }
    else {
    
    
        this->ghost_map.erase(L->key);
        // 注意: 由于之后指针会往主list中放, 不释放指针
        if (this->ghost_size == 1) {
    
    
            this -> ghost = nullptr;
        }
        else if (L == ghost) {
    
     
            // 删除头结点
            CacheList* p = this->ghost;
            this->ghost = L->next; // 重置
            ghost->prev = L->prev;
            L->prev->next = ghost;
        }
        else {
    
     // 连接前后节点
            CacheList* q = L->prev;
            q->next = L->next;
            L->next->prev = q;
        }
        // 重置L指针以便后续插入
        L->prev = nullptr;
        L->next = nullptr;
        L->time = 1;            // 将time重置为1
        ghost_size -= 1;
    }
}

// 删除尾部元素并删除哈希表对应元素, 如果是删除主表的元素,则加入ghost中
void LRUcache::DeleteTail(bool is_ghost) {
    
    
    if (!is_ghost) {
    
    
        CacheList* remove;
        if (!this->cache) throw("no element to delete");
        else if (this->size == 1) {
    
    
            remove = cache;
            this -> cache = nullptr; // 更新头结点
        }
        else {
    
    
            // 移除 cache的prev节点即尾结点
            remove = cache->prev;
            CacheList* q = remove->prev;
            q->next = cache;
            cache->prev = q;
        }
        // 重整remove节点
        remove->prev = nullptr; remove->next = nullptr; remove->time = 0;
        // 清除哈希表中的元素
        map.erase(remove->key);

        if (this -> ghost_size == this -> ghost_capacity) DeleteTail(true);
        // 对于ghost缓存满,则先移除元素, 再插入到ghost数组中, 不满则不移除元素
        // 然后将元素加入ghost中,显然ghost中是没有对应的项的
        this -> ghost_map[remove->key] = remove;
        Insert(remove, true);
        this->size -= 1;
    }
    else {
    
     // 删除ghost数组中的尾部并移除
        if (!this->ghost) throw("no element to delete");
        CacheList* remove;
        if (this->ghost_size == 1) {
    
     
            // 对于ghost中有一个元素并被取出时,删除头结点
            remove = this -> ghost;
            this->ghost = nullptr;
        }
        else {
    
    
            remove = ghost->prev;
            CacheList* q = remove->prev;
            q->next = ghost;
            ghost->prev = q;
        }
        ghost_map.erase(remove->key); // 删除哈希表元素
        delete remove; // 释放存储空间
        this -> ghost_size -= 1;
    }
}

// 插入元素,并且扩容一格(注意需要判断最大空间)
void LRUcache::Add(DataType data) {
    
    
    // 这个对应的是在ghost中找到的情况,需要删除ghost中对应的节点
    this->capacity += 1;
    CacheList* L = new CacheList();
    L->key = data;
    L->time = 1;
    this->map[data] = L; // 直接塞入哈希表
    Insert(L, false);    // 插入节点 
}

// 删除最后一个节点,并且容量缩减一格, 返回是否成功缩减subtract
bool LRUcache::Subtract() {
    
    
    if (this->capacity == 0) return false;
    // 如果表满,则删除节点并放入ghost数组, 否则不用删除节点
    if (this -> size == this ->capacity) DeleteTail(false);  
    this->capacity -= 1;
    return true;
}

// 在ghost中进行查找,如果有则删除对应ghost中的元素并返回true(接下来总函数会调用put)
bool LRUcache::check_ghost(DataType data) {
    
    
    // 注意使用find而不使用ghost_map[data]; 因为这样会新增元素
    if (this->ghost_map.find(data)!= this ->ghost_map.end()) {
    
    
        DetachNode(ghost_map[data], true);
        return true;
    }
    return false;
}

void LRUcache::Show(bool show_ghost) {
    
    
    CacheList* p = this->cache;
    cout << "=============== Last recently used =========" << endl;
    if (!p) {
    
    
        cout << "----- empty list : nothing to show -----" << endl;
        return;
    }
    for (; p->next!= cache;p = p->next) {
    
    
        cout << "key : " << p->key << " , time : " << p->time << endl;
    }
    cout << "key : " << p->key << " , time : " << p->time << endl;
    if (show_ghost) {
    
    
        cout << "------------ghost is ----------------" << endl;
        CacheList* p = ghost; 
        if (!p) return;
        for (; p->next != ghost; p = p->next) {
    
    
            cout << "key : " << p->key << " , time : " << p->time << endl;
        }
        cout << "key : " << p->key << " , time : " << p->time << endl;
    }
    cout << "=============== end ====================" << endl;
}

LFUfunc.cpp

#include "ARCcacheHeader.h"

// 插入元素,其中插入的次数直接设为transform_time
void LFUcache::put(DataType data) {
    
    
    if (this->capacity == 0) return; // 不能加入元素
    if (this -> map.find(data)!= this->map.end()) {
    
    
        // 列表中有该元素
        CacheList* L = map[data];
        this -> DetachNode(L, false);
        L->time += 1;   // 增加使用次数标记
        Insert(L, false);
        cout << "after insert:" << L->time << endl;
    }
    else {
    
     // 没有找到, 考虑删除或者插入元素
        CacheList* L = new CacheList();
        if (this->size == this->capacity) {
    
    
            DeleteTail(false); // 删除一个元素并加入ghost中
        }
        L->key = data;
        L->time = this -> transform_time;
        
        cout << "putting : " << L->key << "the time is " <<  this -> transform_time << endl;
        this->map[data] = L;
        Insert(L, false);
    }
}

// 插入函数, 将节点插入表中,插入方法是有序插入
void LFUcache::Insert(CacheList* L, bool is_ghost) {
    
    
    if (!is_ghost) {
    
    
        // 有序插入算法
        if (!cache) {
    
     // 空则建立结点
            this -> cache = L;
            cache->next = cache;
            cache->prev = cache;
        }
        else if (cache->time < L->time) {
    
     // 插入到头结点上
            CacheList* p = cache->prev;
            L->next = cache;
            L->prev = p;
            p->next = L;
            cache->prev = L;
            this->cache = L; // 更新头结点
        }
        else {
    
    
            CacheList* pre = cache;
            // 在循环链表中有序插入元素 -> 此处有不足,如果使用双哈希表可以得到O(1)的复杂度
            for (; pre->next != cache && pre ->next-> time > L->time; pre = pre->next);
            // 最后停在最末端元素上,将L插入到pre的尾部即可
            CacheList* q = pre->next;
            L->prev = pre;
            L->next = q;
            pre->next = L;
            q->prev = L;
        }
        this->size += 1;
    }
    else{
    
     // 将节点插入ghost
        if (!ghost) {
    
    
            this->ghost = L;
            ghost->next = ghost;
            ghost->prev = ghost;
        }
        else if (ghost->time < L->time) {
    
    
            CacheList *p = ghost->prev;
            L->next = ghost;
            L->prev = p;
            p->next = L;
            ghost->prev = L;
            this->ghost = L;  // 更新头结点
        }
        else {
    
    
            CacheList* pre = ghost;
            // 在循环链表中有序插入元素
            for (; pre->next != ghost && pre->next->time > L->time; pre = pre->next);
            // 将L插入到pre的尾部即可
            CacheList* q = pre->next;
            L->prev = pre;
            L->next = q;
            pre->next = L;
            q->prev = L;
        }
        this->ghost_size += 1;
    }
}

// 添加元素并且增加空间
void LFUcache::Add(DataType data) {
    
    
    this->capacity += 1;
    CacheList* L = new CacheList();
    L->key = data;
    L->time = 1;  // 此时调用的次数设为1
    this->map[data] = L;
    Insert(L, false);
}

// 删除尾元素
void LFUcache::DeleteTail(bool is_ghost) {
    
    
    if (!is_ghost) {
    
    
        CacheList* remove;
        if (!this->cache) throw("no element to delete");
        // 删除节点,哈希值并将其移动到ghost表中
        else if (this->size == 1) {
    
     //删除头结点
            remove = cache;
            this->cache = nullptr;
        }
        else {
    
     // 移除尾结点
            remove = cache->prev;
            CacheList* q = remove->prev;
            q->next = cache;
            cache->prev = q;
        }
        remove->prev = nullptr; remove->next = nullptr; remove->time = 0;
        map.erase(remove->key);
        if (this->ghost_size == this->ghost_capacity) DeleteTail(true);
        
        this -> ghost_map[remove->key] = remove;   // 加入到ghost_map 的哈希表中
        Insert(remove, true);
        this->size -= 1;
    }
    else {
    
     // 删除ghost中的结尾元素
        if (!this->ghost) {
    
    
            throw("no element to delete");
        }
        CacheList* remove;
        if (this->ghost_size == 1) {
    
    
            remove = ghost;
            this->ghost = nullptr;
        }
        else {
    
    
            remove = ghost->prev;
            CacheList* q = remove->prev;
            q->next = ghost;
            ghost->prev = q;
        }
        ghost_map.erase(remove->key);
        delete remove;  // 释放指针空间和哈希表空间
        this->ghost_size -= 1;
    }

}

// 主表时(put调用)往其他位置有序insert, ghost表时(仅有check_ghost调用),向主表中insert
void LFUcache::DetachNode(CacheList*L, bool is_ghost) {
    
    
    // Detach操作相同,直接复制粘贴FRUfunc的代码
    if (!is_ghost) {
    
    
        if (this->size == 0) {
    
    
            throw runtime_error("nothing to detach!");
        }
        if (this->size == 1) {
    
    
            this->cache = nullptr;
        }
        else if (L == cache) {
    
     // 暂时清除头结点
            this->cache = L->next;
            cache->prev = L->prev;
            L->prev->next = cache;
        }
        else {
    
     // 取出中间节点
            CacheList* q = L->prev;
            q->next = L->next;
            L->next->prev = q;
        }
        L->prev = nullptr; L->next = nullptr;
        size -= 1;
    }
    else {
    
    
        ghost_map.erase(L->key);
        // 注意: 由于之后指针会往主list中放, 不释放指针
        if (this->ghost_size == 1) {
    
    
            this->ghost = nullptr;
        }
        else if (L == ghost) {
    
    
            // 删除头结点
            CacheList* p = this->ghost;
            this->ghost = L->next; // 重置
            ghost->prev = L->prev;
            L->prev->next = ghost;
        }
        else {
    
     // 连接前后节点
            CacheList* q = L->prev;
            q->next = L->next;
            L->next->prev = q;
        }
        // 重置L指针以便后续插入
        L->prev = nullptr;
        L->next = nullptr;
        L->time = 1;            // 将time重置为1
        ghost_size -= 1;
    }
}

// 删除结尾元素并减少容量
bool LFUcache::Subtract() {
    
    
    if (this ->capacity == 0) return false;
    // 如果表满,则删除节点并放入ghost数组, 否则不用删除节点
    if (this ->size == this ->capacity) DeleteTail(false);
    this->capacity -= 1;
    return true;
};

// 在ghost中进行查找,如果有则删除对应ghost中的元素并返回true(接下来总函数会调用put)
bool LFUcache::check_ghost(DataType data) {
    
    
    if (ghost_map.find(data)!= ghost_map.end()) {
    
    
        this ->DetachNode(ghost_map[data], true);
        return true;
    }
    return false;
}

// 显示缓存内容
void LFUcache::Show(bool show_ghost) {
    
    
    CacheList* p = this->cache;
    cout << "=============== Last frequently used =========" << endl;
    if (!p) {
    
    
        cout << "----- empty list : nothing to show -----" << endl;
        return;
    }
    for (; p->next != cache; p = p->next) {
    
    
        cout << "key : " << p->key << " , time : " << p->time << endl;
    }
    cout << "key : " << p->key << " , time : " << p->time << endl;
    if (show_ghost) {
    
    
        cout << "------------ghost is ----------------" << endl;
        CacheList* p = ghost;
        if (!p) return;
        for (; p->next != ghost; p = p->next) {
    
    
            cout << "key : " << p->key << " , time : " << p->time << endl;
        }
        cout << "key : " << p->key << " , time : " << p->time << endl;
    }
    cout << "=============== end ====================" << endl;
}

ARCfunc.cpp

#include "ARCcacheHeader.h"

// 将元素输入ARC缓存表中
void ARCcache::put(DataType data) {
    
    
    bool find = false;
    // 先在各个表的ghost中搜索
    if (this->Rcache->check_ghost(data)) {
    
     // 这个删除
        // 缩减Fcache, 如果不成功,则调用put函数
        if (this->Fcache->Subtract()) {
    
    
            this->Rcache->Add(data);  // 扩容Rcache
        }
        else {
    
     // Fcache缩容失败,此时直接加入Rcache中
            this->Rcache->put(data);
            // 进行put一次,由于在ghost列表中找到,
            // 原先数组中应该没有这个数,理论上不用if
        }
        find = true;
    }
    if (this->Fcache->check_ghost(data)) {
    
    
        if (this->Rcache->Subtract()) {
    
    
            this->Fcache->Add(data); // 扩容
        }
        else {
    
    
            this->Fcache->put(data); // 不扩容直接放
        }
        find = true;
    }

    // 如果两个的ghost中都没有对应的数, 则不改变partition(两个容量不改变)
    if (!find) {
    
    
        // 优先加入LRUcache中, 作为第一个元素
        if (this->Rcache->put(data)) this->Fcache->put(data);
        // 使用put函数向Fcache中也添加对应元素
    }
}

void ARCcache::Show(bool show_ghost) {
    
    
    cout << "%%%%%%%%%%%%%%%%%%% Begin %%%%%%%%%%%%%%%%%%%%%%" << endl;
    this->Rcache->Show(show_ghost);
    this->Fcache->Show(show_ghost);
    cout << "%%%%%%%%%%%%%%%%%%%% end %%%%%%%%%%%%%%%%%%%%%%%" << endl;
}

六、运行界面

你每次输入一个词语,会自动统计词频并且显示缓存中的词语, 下面是输入一个hello和一个world
在这里插入图片描述
下面的代码是输入world 并触发LRU的ghost_cache,然后Last_recently_used 的空间从5个变为6个
在这里插入图片描述

当然了,这个程序没有经过太多测试,可能对于部分刁钻的数据有可能之后会出现报错现象, 所以代码仅供参考学习

这一期就到这里辣!

猜你喜欢

转载自blog.csdn.net/sbsbsb666666/article/details/130198264
今日推荐