首页 > 编程语言 >LRU 算法与 LFU 算法

LRU 算法与 LFU 算法

时间:2023-06-14 10:36:58浏览次数:39  
标签:node 链表 int Node LFU 算法 LRU key freq

算法介绍

LRU

LRU 全称是 Least Recently Used,即最近最久未使用算法。

LRU 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高,它是页面置换算法的一种,也常用于缓存设计。

LFU

LFU 全称是 Least Frequently Used,根据频率来选择要淘汰的元素,即删除访问频率最低的元素,如果两个元素的访问频率相同,则淘汰访问频率最低的元素中最久没有被访问的元素。

数据结构

不管是 LRU 还是 LFU 算法,我们都需要使用到双向链表作为基础数据结构,由于 LRULFU 涉及的对双向链表的元素的操作比较复杂,还涉及对链表结点的其他操作,因此选择自己手写一个简单的双向链表,同时复习双向链表的实现(阿里一面就被问到了,半天没写对)。

这里根据 LRULFU 的需要,简单封装了删除结点、尾部插入结点、和判断双向链表是否为空三个函数,很大程度上简化了 LRULFU 的实现,降低了实现的出错概率。

struct Node {
    Node() {
    }
    Node(int val, int key) :
        val_(val), key_(key), next_(nullptr), pre_(nullptr) {
    }
    int val_;
    int freq_;
    Node *next_;
    Node *pre_;
    int key_;
};
struct List {
    Node *vhead_;  // 虚拟头结点
    Node *vtail_;  // 虚拟尾结点
    int size_ = 0; // 链表中有效结点的数量
    List() :
        vhead_(new Node()), vtail_(new Node()) {
        vhead_->next_ = vtail_;
        vtail_->pre_ = vhead_;
        vhead_->pre_ = nullptr;
        vtail_->next_ = nullptr;
    }
    ~List() {
        delete vtail_;
        delete vhead_;
        vhead_ = nullptr;
        vtail_ = nullptr;
    }
    void Insert(Node *node) {
        // 双向链表的插入, node 表示待插入结点,插入作为双向链表的尾结点
        node->pre_ = vtail_->pre_;
        vtail_->pre_->next_ = node;
        vtail_->pre_ = node;
        node->next_ = vtail_;
        ++size_;
    }
    void Delete(Node *node) {
        // node 指向待删除结点
        node->next_->pre_ = node->pre_;
        node->pre_->next_ = node->next_;
        --size_;
    }
    bool Empty() {
        return size_ <= 0;
    }
};

LRU 实现

对于 LRU 的实现,我们需要借助两个数组结构哈希表双向链表来组成一个新的数据结构。我们利用哈希表实现 $O(1)$ 时间复杂度的查找,获取元素的 val 以及在双向链表中的位置;利用双向链表实现 $O(1)$ 时间复杂度内的元素插入和删除。

  • 删除元素时,要么通过哈希表获取了待删除元素在链表中的位置,要么是删除头结点;
    • 删除元素后,需要在哈希表中也删除该元素,因此 Node 需要有 key_ 成员;
  • 插入元素则都是插入到链表的末尾结点。

注意插入和删除时的结点数量变化,只有当结点数量小于 capacity 时插入结点需要递增 cnt_

class LRUCache {
  public:
    LRUCache(int capacity) :
        lst(new List()), cap_(capacity) {
    }

    int get(int key) {
        if (hash.find(key) == hash.end()) {
            return -1;
        }
        Node *node = hash[key];
        lst->Delete(node);
        lst->Insert(node);
        return node->val_;
    }

    void put(int key, int value) {
        if (hash.find(key) != hash.end()) {
            // key 已经存在
            Node *node = hash[key];
            node->val_ = value;
            node->key_ = key;
            get(key);
            return;
        }
        // key 不存在,需要插入
        if (cnt_ == cap_) {
            // 这里先插入或者先删除都能满足题义,我选择先插入,再删除(防止 capacity 为 0 的极限情况)
            Node *node = new Node(value, key);
            lst->Insert(node);
            hash[key] = node;

            // 删除头结点
            Node *head = lst->vhead_->next_;
            lst->Delete(head);
            hash.erase(head->key_);
            delete head;
            head = nullptr;
        } else {
            // 直接插入即可
            Node *node = new Node(value, key);
            lst->Insert(node);
            hash[key] = node;
            // 记得修改数量
            ++cnt_;
        }
    }

  private:
    unordered_map<int, Node *> hash;
    List *lst;
    int cnt_ = 0;
    int cap_;
};

LFU 实现

LFU 的实现比起 LRU 来说可要复杂太多了,要注意的地方也很多。LFU 需要用到两个哈希表,$N$ 个双向链表。

  • 第一个哈希表是 key-node 哈希表,如下图所示,key 就是输入元素的 key哈希表 1

  • 第二个哈希表则是 Freq-List 哈希表,key 是元素的使用频率,value 是指向双向链表的指针,该双向链表与 LRU 中的双向链表形式类似。 B8mysGRN2HQtjOz

将两个哈希表看起来看一下完整的数据结构: VjQdHYSM9vwBoPq

LFU 实现时,我们针对 get 操作和 put 操作分别讨论需要注意的地方。

get 操作相比 put 要简单一点,与 LRU 中的 get 操作类似,

  • 区别在于要注意 get(key) 时,与 key 对应的 nodefreq_ 需要递增,因此,还需要变更频率哈希表和它对应的链表;
    • 如果链表变成了空链表,需要从频率哈希表中移除空链表对应的频率

put 操作则要复杂很多:

  • 如果 key 已经存在缓存中了,那么执行一次 get(key) 再更新一下 node 中的 val_ 即可;
  • 如果 key 不存在于缓存中,那么我们要分缓存已满和缓存未满两种情况来讨论:
    • 如果缓存已满,即 cnt_ == cap_,那么我们先执行删除结点,删除结点后,如果 min_freq_ 对应的链表变成了空链表,那么就要 delete 该链表,并且从频率哈希表中移除该最小频率;然后插入该结点(结点频率为 1),并且更新 min_freq_ 为 1;
    • 如果缓存未满,我们插入该结点(结点频率为 1),并且更新 min_freq_ 为 1。
class LFUCache {
  public:
    LFUCache(int capacity) :
        cap_(capacity), min_freq_(0), cnt_(0) {
    }

    int get(int key) {
        if (hash_.find(key) == hash_.end()) {
            return -1;
        }
        Node *node = hash_[key];
        int freq = node->freq_;
        // 要更新频率,因此要从原先的频率链表上删除该结点
        freqs_[freq]->Delete(node);
        if (freqs_[freq]->Empty()) {
            // 删除该链表,频率哈希表中移除该频率
            delete freqs_[freq];
            freqs_.erase(freq);
            if (min_freq_ == freq) {
                // 则需要更新最小频率
                min_freq_ = freq + 1;
            }
        }
        // 更新频率
        ++node->freq_;
        freq = node->freq_;
        if (freqs_.find(freq) == freqs_.end()) {
            freqs_[freq] = new List();
        }
        freqs_[freq]->Insert(node);
        return node->val_;
    }

    void put(int key, int value) {
        // key 已经在缓存中了
        if (hash_.find(key) != hash_.end()) {
            get(key);
            hash_[key]->val_ = value;
            return;
        }
        /* key 不在缓存中 */
        // 缓存已满
        if (cnt_ == cap_) {
            // 删除 min_freq_ 链表对应的头结点
            List *lst = freqs_[min_freq_];
            Node *to_del = lst->vhead_->next_;
            lst->Delete(to_del);
            hash_.erase(to_del->key_);
            delete to_del;
            to_del = nullptr;
            // 如果 lst 变为空
            if (lst->Empty()) {
                delete lst;
                lst = nullptr;
                freqs_.erase(min_freq_);
            }
            // 现在执行插入
            Node *node = new Node(value, key);
            node->freq_ = 1;
            min_freq_ = 1;
            if (freqs_.find(node->freq_) == freqs_.end()) {
                freqs_[node->freq_] = new List();
            }
            freqs_[node->freq_]->Insert(node);
            hash_[key] = node;
        } else {
            // 现在执行插入
            Node *node = new Node(value, key);
            node->freq_ = 1;
            min_freq_ = 1;
            if (freqs_.find(node->freq_) == freqs_.end()) {
                freqs_[node->freq_] = new List();
            }
            freqs_[node->freq_]->Insert(node);
            hash_[key] = node;
            ++cnt_;
        }
    }

  private:
    unordered_map<int, Node *> hash_;
    unordered_map<int, List *> freqs_;
    int min_freq_;
    int cnt_;
    int cap_;
};

这里还要注意的是,类中 int 成员变量,如果没有在构造函数中显式执行初始化,其默认值很可能不是0。

标签:node,链表,int,Node,LFU,算法,LRU,key,freq
From: https://www.cnblogs.com/zwyyy456/p/17479465.html

相关文章

  • kmp 算法
    问题描述kmp算法解决的是字符串匹配问题,即:字符串P是否是字符串S的子串?如果是,它出现在s的哪些位置?这里我们称S为主串,P为模式串。思路首先是暴力匹配算法(Brute-Force算法),代码如下:voidBruteForce(strings,stringp){intlen_s=s.size(),len_p=p.size();fo......
  • 【基础算法】单链表的OJ练习(2) # 链表的中间结点 # 链表中倒数第k个结点 #
    前言对于单链表的OJ练习,<fontcolor=bluesize=4>需要深刻理解做题的思路</font>,这样我们才能够在任何场景都能够熟练的解答有关链表的问题。关于OJ练习(1):==->==传送门==<-==,其题目较为简单,思路也好理解,本章与(1)差不多,难度不大,<fontcolor=orangesize=4>核心点就在于理解解......
  • 马拉车算法
    截图来自董老师https://www.bilibili.com/video/BV173411V7Ai/?spm_id_from=333.999.0.0&vd_source=23dc8e19d485a6ac47f03f6520fb15c2  P3805【模板】manacher算法#include<iostream>#include<cstring>#include<cmath>#include<algorithm>#include......
  • 【VRP问题】基于遗传算法求解多约束多无人机灾情应急救援路径规划问题附matlab代码
    ✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。......
  • 多种智能优化算法运行时间和不同函数测试对比附matlab代码
    ✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。......
  • 《数据结构与算法》之二叉树(补充树)
    一.树结构之二叉树操作二叉树的查找二叉搜索树,也称二叉排序树或二叉查找树二叉搜索树:一棵二叉树,可以为空,如果不为空,应该满足以下性质:非空左子树的所有结点小于其根结点的键值非空右子树的所有结点大于其根结点的键值左右子树都是二叉搜索树对于二叉树的查找,其实沿用的是......
  • 文心一言 VS 讯飞星火 VS chatgpt (40)-- 算法导论5.4 4题
    四、一次聚会需要邀请多少人,才能让其中3人的生日很可能相同?文心一言:为了解决这个问题,我们需要先计算出所有可能的生日组合数,然后计算出其中生日相同的组合数。已知需要邀请的人数为:23人根据组合数公式,可计算出所有可能的生日组合数:2.58520167*10^{22}根据生日相同的组合......
  • 文心一言 VS 讯飞星火 VS chatgpt (40)-- 算法导论5.4 4题
    四、一次聚会需要邀请多少人,才能让其中3人的生日很可能相同?文心一言:为了解决这个问题,我们需要先计算出所有可能的生日组合数,然后计算出其中生日相同的组合数。已知需要邀请的人数为:23人根据组合数公式,可计算出所有可能的生日组合数:2.58520167*10^{22}根据生日相同的组合数公式,可......
  • .NET编译项目时出现《此实现不是 Windows 平台 FIPS 验证的加密算法的一部分》处理方
    此实现不是Windows平台FIPS验证的加密算法的一部分。”)的问题,如下图所示:对于上面的问题,只需要修改下注册表即可处理,方法如下:1、以管理员方式启动命令行工具后输入regedit,回车打开注册器;。2、打开注册表后,进入路径:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Co......
  • java常见的排序算法(冒泡排序、选择排序、插入排序、shell排序、归并排序、堆排序、快
    (文章目录)本文简单的介绍了java常见的几种排序。所有的排序均是一个数组由小到大进行排序。一、冒泡排序1、效率表现和适用范围效率O(n²)适用于排序小列表2、算法实现publicvoidbubbleSortArray(int[]a){ intn=a.length; for(inti=1;i<n;i++){ fo......