C++ 实现LRU 与 LFU 的缓存算法

C++ 实现LRU 与 LFU 的缓存算法

算法描述

LRU和LFU是常用的缓存算法。它们能够优化系统读写速度,提高系统效率。

LRU

LRU (Least Recent Used)是最近最少使用算法,维护一个缓存队列,每次访问缓存中的一个元素时,将其移动到队列的头部,当缓存队列满时删除队尾元素,保证最近使用过的元素在缓存队列的最前面,最近没有使用过的元素在缓存队列的最后面。

LFU

LFU (Least Frequently Used)是利用元素访问次数的算法,维护一个缓存队列,每次访问缓存中的一个元素时,将该元素的访问次数增加1。当缓存队列满时,删除访问次数最少的元素。这种算法可以有效地处理热点数据的缓存问题。

算法实现

下面分别实现LRU和LFU算法。

LRU

使用哈希表和双向链表实现LRU算法,哈希表维护元素在链表中的位置,双向链表维护元素的访问顺序。

class Node { // 链表节点
public:
    int key, val;
    Node* prev;
    Node* next;
    Node(int k, int v) : key(k), val(v), prev(nullptr), next(nullptr) {}
};

class LRUCache {
public:
    LRUCache(int capacity) : cap(capacity) {}

    int get(int key) {
        if (!hash.count(key)) return -1; // 未找到元素
        Node* node = hash[key]; // 取出节点
        update(node); // 更新节点访问顺序
        return node->val;
    }

    void put(int key, int value) {
        if (hash.count(key)) {
            Node* node = hash[key]; // 取出节点
            node->val = value; // 更新节点
            update(node); // 更新节点访问顺序
        }
        else {
            if (list.size() == cap) { // 缓存队列已满
                Node* node = list.back(); // 取出队尾节点
                hash.erase(node->key); // 从哈希表中删除
                list.pop_back(); // 从链表中删除
                delete node; // 释放节点
            }
            Node* node = new Node(key, value); // 创建新节点
            list.push_front(node); // 新节点插入到队头
            hash[key] = node; // 将新节点加入哈希表
        }
    }

private:
    int cap; // 缓存容量大小
    list<Node*> list; // 双向链表,维护元素访问顺序
    unordered_map<int, Node*> hash; // 哈希表,记录元素在链表中的位置

    // 更新节点访问顺序
    void update(Node* node) {
        list.erase(node); // 先将节点从链表中删除
        list.push_front(node); // 将节点插入到队头
    }
};

LFU

使用哈希表、桶和两个双向链表实现LFU算法,哈希表维护元素在桶中的位置,每个桶记录访问次数相同的元素,一个双向链表维护桶的访问顺序,另一个双向链表维护元素的访问顺序。

class Node { // 链表节点
public:
    int key, val, freq;
    Node* prev;
    Node* next;
    Node(int k, int v, int f) : key(k), val(v), freq(f), prev(nullptr), next(nullptr) {}
};

class LFUCache {
public:
    LFUCache(int capacity) : cap(capacity), minFreq(0) {}

    int get(int key) {
        if (!hash.count(key)) return -1; // 未找到元素
        Node* node = hash[key]; // 取出节点
        update(node); // 更新节点访问顺序
        return node->val;
    }

    void put(int key, int value) {
        if (cap == 0) return;
        if (hash.count(key)) {
            Node* node = hash[key]; // 取出节点
            node->val = value; // 更新节点
            update(node); // 更新节点访问顺序
        }
        else {
            if (hash.size() == cap) { // 缓存队列已满
                DLList& list = freq[listHead->next->freq]; // 取出访问频率最小的桶
                Node* node = list.removeTail(); // 取出队尾节点
                hash.erase(node->key); // 从哈希表中删除
                delete node; // 释放节点
            }
            Node* node = new Node(key, value, 1); // 创建新节点
            freq[1].addHead(node); // 将新节点插入到访问频率为1的桶的头部
            hash[key] = node; // 将新节点加入哈希表
            listHead = &freq[1]; // 更新访问频率最小的桶
        }
    }

private:
    int cap; // 缓存容量大小
    int minFreq; // 最小访问频率
    DLList* listHead; // 访问频率最小的桶
    unordered_map<int, Node*> hash; // 哈希表,记录元素在桶中的位置
    unordered_map<int, DLList> freq; // 桶,记录访问频率相同的元素

    class DLList { // 双向链表
    public:
        DLList() : head(new Node(0, 0, 0)), tail(new Node(0, 0, 0)), size(0) { // 头尾哨兵节点
            head->next = tail;
            tail->prev = head;
        }

        void addHead(Node* node) { // 在头部添加节点
            node->next = head->next;
            node->prev = head;
            head->next->prev = node;
            head->next = node;
            ++size;
        }

        Node* removeTail() { // 删除尾部节点
            if (size == 0) return nullptr;
            Node* node = tail->prev;
            remove(node);
            return node;
        }

        void remove(Node* node) { // 删除指定节点
            node->prev->next = node->next;
            node->next->prev = node->prev;
            --size;
        }

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

    private:
        Node* head; // 头哨兵节点
        Node* tail; // 尾哨兵节点
        size_t size; // 链表长度
    };

    // 更新节点访问顺序
    void update(Node* node) {
        DLList& list = freq[node->freq]; // 取出节点所在的桶
        list.remove(node); // 从当前桶中删除节点
        if (list.empty() && node->freq == minFreq) { // 当前桶为空且节点所在桶是访问频率最小的桶
            ++minFreq; // 最小访问频率加1
            listHead = &freq[minFreq]; // 更新访问频率最小的桶
        }
        ++node->freq; // 节点访问频率加1
        freq[node->freq].addHead(node); // 将节点加入新桶的头部
    }
};

示例说明

下面给出两个示例说明。

示例1

LRUCache lruCache(2);
lruCache.put(1, 1);
lruCache.put(2, 2);
assert(lruCache.get(1) == 1); // 返回 1
lruCache.put(3, 3); // 缓存容量已满,需要删除最近最少使用的元素 2
assert(lruCache.get(2) == -1); // 返回 -1,因为元素 2 已经被删除
lruCache.put(4, 4); // 缓存容量已满,需要删除最近最少使用的元素 1
assert(lruCache.get(1) == -1); // 返回 -1,因为元素 1 已经被删除
assert(lruCache.get(3) == 3); // 返回 3
assert(lruCache.get(4) == 4); // 返回 4

示例2

LFUCache lfuCache(2);
lfuCache.put(1, 1);
lfuCache.put(2, 2);
assert(lfuCache.get(1) == 1); // 返回 1
lfuCache.put(3, 3); // 缓存容量已满,需要删除访问频率最小的元素 2
assert(lfuCache.get(2) == -1); // 返回 -1,因为元素 2 已经被删除
assert(lfuCache.get(3) == 3); // 返回 3
lfuCache.put(4, 4); // 缓存容量已满,需要删除访问频率最小的元素 1
assert(lfuCache.get(1) == -1); // 返回 -1,因为元素 1 已经被删除
assert(lfuCache.get(3) == 3); // 返回 3
assert(lfuCache.get(4) == 4); // 返回 4
lfuCache.put(3, 33); // 更新元素 3 的值,访问频率加1
assert(lfuCache.get(3) == 33); // 返回 33
assert(lfuCache.get(4) == 4); // 返回 4
lfuCache.put(2, 22); // 更新元素 2 的值,访问频率加1
assert(lfuCache.get(2) == 22); // 返回 22

以上就是C++实现LRU与LFU的缓存算法的完整攻略。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C++ 实现LRU 与 LFU 的缓存算法 - Python技术站

(0)
上一篇 2023年5月22日
下一篇 2023年5月22日

相关文章

  • C++递归算法实例代码

    C++递归算法是指函数内部调用自身的方法,用来解决复杂的问题。在编写递归算法时,首先需要确定递归基(即结束条件),然后通过递归调用不断缩小问题规模,直到达到递归基结束递归。下面是C++递归算法的实例代码: 一、递归实现斐波那契数列 斐波那契数列是指数列中每个数都是前两个数的和。下面是用递归实现斐波那契数列的代码: int fibonacci(int n) {…

    C 2023年5月22日
    00
  • Java中的StackOverflowError错误问题及解决方法

    Java中的StackOverflowError错误问题及解决方法 在Java开发中,如果递归调用方法过多,可能会导致StackOverflowError错误。本文将详细介绍如何识别该错误以及如何解决该问题。 StackOverflowError错误 当调用堆栈的大小超过JVM允许的最大深度时,就会发生StackOverflowError错误,即递归调用过于…

    C 2023年5月23日
    00
  • 拳皇97大门bug震的全人物整理

    拳皇97大门bug震的全人物整理攻略 什么是大门bug震? 在拳皇97中,存在一个被称为“大门bug”的漏洞。使用此漏洞可以通过特定按键组合让对手的活力值瞬间降为0,从而轻松获胜。而“大门bug震”则是一种利用此漏洞的特定攻击方式,使整个对手团队都受到震动效果,从而更容易实现胜利。 如何进行“大门bug震”? 要进行“大门bug震”,需要先使用一定的招数组合…

    C 2023年5月22日
    00
  • 详解ubuntu安装opencv的正确方法

    详解Ubuntu安装OpenCV的正确方法 OpenCV是一个非常流行的开源计算机视觉库,它能够处理各种图像和视频处理任务。本文将详细介绍Ubuntu系统中安装OpenCV的正确方法。 步骤1:更新系统软件包 在安装OpenCV之前,我们需要确保系统中的软件包是最新的。为此,我们可以使用以下命令更新软件包: sudo apt update sudo apt …

    C 2023年5月22日
    00
  • 结合Mybatis聊聊对SQL注入的见解

    结合MyBatis聊聊对SQL注入的见解 什么是SQL注入? SQL注入(SQL Injection),也称为SQL攻击,是一种代码注入攻击。攻击者利用Web应用程序通过将恶意的SQL代码注入到输入字段中来攻击后台数据库服务器,从而获得敏感信息或者完全控制后台数据库。这些注入代码可能在数据请求中或者输入URL参数中出现。SQL注入是当前Web应用程序的最大安…

    C 2023年5月22日
    00
  • JS跨域交互(jQuery+php)之jsonp使用心得

    下面我为你讲解一下“JS跨域交互(jQuery+php)之jsonp使用心得”的完整攻略。 什么是跨域? 跨域(cross-origin)是指在当前请求资源(如 javascript、css、json、xml 等)的文档或脚本所属窗口(window、iframe 或 frame)与请求资源所在文档的域(domain)不同情况下的访问。 JSONP 原理 JS…

    C 2023年5月23日
    00
  • C++基本算法思想之穷举法

    C++基本算法思想之穷举法攻略 穷举法概述 穷举法是一种基本的算法思想,也称为暴力搜索或枚举搜索,是一种对所有可能性进行逐一验证的算法。它通过枚举问题所有可能的解,来寻找问题的最优解。 穷举法的具体步骤 穷举法的具体步骤可以分为三部分: 1. 确定问题的解空间 问题的解空间是指问题的所有可能解构成的集合。在使用穷举法解决问题时,需要确定问题的解空间,以便于后…

    C 2023年5月22日
    00
  • C语言如何实现Unix时间戳与本地时间转化

    C语言提供了一些标准库函数,可以用来实现Unix时间戳与本地时间的转换。下面是实现这个功能的完整攻略: 获取Unix时间戳 Unix时间戳是指从1970年1月1日开始经过的秒数。在C语言中,可以使用time()函数获取当前的Unix时间戳。time()函数的定义如下: #include <time.h> time_t time(time_t *t…

    C 2023年5月23日
    00
合作推广
合作推广
分享本页
返回顶部