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++11 lambda表达式/包装器/线程库

    深入解析C++11 lambda表达式/包装器/线程库 C++11 lambda表达式 Lambda表达式是C++11中最重要的新特性之一。Lambda表达式提供了一种简单且易于使用的方式,用于定义和传递匿名的、可调用的代码块。 基本语法 Lambda表达式的基本语法如下: [capture list] (params) -> return_type …

    C 2023年5月22日
    00
  • 如何用C++制作LeetCode刷题小技巧-错题记录本

    下面是针对“如何用C++制作LeetCode刷题小技巧-错题记录本”的完整攻略,具体步骤如下: 步骤一:创建一个C++项目 首先,打开你喜欢的C++ IDE,创建一个新项目。你可以使用任何你想用的IDE,比如 Visual Studio、Code::Blocks、Dev-Cpp等等。 步骤二:下载LeetCode的数据结构 在C++中,数据结构非常重要。因此…

    C 2023年5月23日
    00
  • 学习C++编程的必备软件

    下面我将为您详细讲解“学习C++编程的必备软件”的完整攻略。 学习C++编程的必备软件 1. C++编译器 C++编译器是你学习编程时必备的工具之一。编译器负责将写好的C++程序翻译成机器可以理解的语言,让计算机可以运行它。以下是几个常用的C++编译器: Visual Studio:Visual Studio是一个非常强大的开发环境,附带了C++编译器和许多…

    C 2023年5月23日
    00
  • C++计数排序详解

    C++计数排序详解 什么是计数排序? 计数排序是一种非比较型排序算法,它的基本思想是统计所有元素的出现次数,然后根据每个元素的出现次数,依次将这些元素放入数组中,从而得到排好序的数组。 计数排序的基本原理 计数排序利用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素个数。然后根据数组C来将A中的元素排到正确的位置。例如,如果C[3]=4,那么值…

    C 2023年5月22日
    00
  • C语言代码实现简单2048游戏

    C语言代码实现简单2048游戏攻略 简介 在这篇攻略中,我将教您如何使用C语言编写简单的2048游戏。2048是一个流行的数字益智游戏,目标是在一个4×4的方格中合并数字,并达到最大的数字2048。在这个过程中,我们将使用C语言并结合控制流和数组等知识点来完成我们的游戏。 步骤 步骤1:定义游戏棋盘 在2048游戏中,我们需要定义一个4×4的棋盘来存储游戏状…

    C 2023年5月23日
    00
  • C++ 中的Lambda表达式写法

    当我们需要在C++中写一些短的、临时的函数时,常常使用Lambda表达式。Lambda表达式可以看作是一个匿名函数,它可以在任意处声明和定义,并且不会产生额外的开销。本文将详细讲解如何在C++中使用Lambda表达式。 基本语法 Lambda表达式的语法如下: [capture clause] (parameters) -> return_type {…

    C 2023年5月22日
    00
  • C语言实现五子棋小游戏

    C语言实现五子棋小游戏攻略 1. 环境准备 在开始编写五子棋小游戏前,需要先确定所用的开发工具以及环境。 1.1 开发工具 可以使用任何一种 C 语言开发工具,如 Visual Studio、Code::Blocks、Dev-C++等。本攻略以 Code::Blocks 为例进行讲解。 1.2 环境配置 安装 Code::Blocks 后,需要进行一些环境配…

    C 2023年5月23日
    00
  • 如何利用OpenGL画坐标轴指示图

    下面是详细的攻略,它包括了OpenGL画坐标轴指示图的完整过程: 准备工作 在开始之前,我们需要安装以下工具: OpenGL库(例如OpenGL ES或OpenGL) 开发环境,例如Visual Studio或Xcode 了解C++语言编程 步骤一:建立OpenGL的环境 我们需要建立OpenGL的环境来画图。在这个步骤中,你需要建立OpenGL窗口并初始化…

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