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

LRU 算法与 LFU 算法

时间:2023-06-13 15:34:17浏览次数:37  
标签: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/17477652.html

相关文章

  • 代码随想录算法训练营第六天| 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数
    454.四数相加II1,难点:1,多个数组之间,会有重复出现的数组,如果单用multiset也是会出错的2,如果用mutliset,在使用distance找出来equal_range的值的时候,也是会出现奇怪的错误的2,正确思路1,把重复出现的节点,次数存放到map种,然后进行遍历3,代码:1intfourSumCount(v......
  • 湖边的烦恼-算法训练题
    湖边的烦恼-算法训练题-递归问题描述每年冬天,北大未名湖上都是滑冰的好地方。北大体育组准备了许多冰鞋,可是人太多了,每天下午收工后,常常一双冰鞋都不剩。每天早上,租鞋窗口都会排起长龙,假设有还鞋的m个,有需要租鞋的n个。现在的问题是,这些人有多少种排法,可以避免出现体育......
  • ChatGPT之问艺道:如何借助神级算法Prompt,让你轻松get到更高质量答案?
    摘要:本文由葡萄城技术团队编写,文章的内容借鉴于IbrahimJohn的《TheArtofAskingChatGPT》(向ChatGPT提问的艺术)。前言当今,ChatGPT赢得越来越多人的青睐,人们通过它输入问题并获取答案。但除了简单的一问一答以外,ChatGPT还有许多隐藏的提问方式,是否想要一探究竟?今天,我们为您......
  • 代码随想录算法训练营第24天 | ● 理论基础 ● 77. 组合 - 第7章 回溯算法part01
     第七章 回溯算法part01今日内容: ●  理论基础 ●  77. 组合    详细布置   理论基础  其实在讲解二叉树的时候,就给大家介绍过回溯,这次正式开启回溯算法,大家可以先看视频,对回溯算法有一个整体的了解。 题目链接/文章讲解:https://programmercar......
  • 代码随想录算法训练营第25天 | ● 216.组合总和III ● 17.电话号码的字母组合 - 第7章
     第七章 回溯算法part02 今日内容:  ●  216.组合总和III●  17.电话号码的字母组合  详细布置   216.组合总和III  如果把 组合问题理解了,本题就容易一些了。  题目链接/文章讲解:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%B......
  • 【基础算法】单链表的OJ练习(1) # 反转链表 # 合并两个有序链表 #
    前言上一章讲解了单链表==->==传送门==<-==,后面几章就对单链表进行一些简单的题目练习,目的是为了更好的理解单链表的实现以及加深对某些函数接口的熟练度。本章带来了两个题目。一是<fontcolor=red>反转链表</font>,二是<fontcolor=red>合并两个有序链表</font>,整体难......
  • 算法题:冒泡排序
    functionbubbleSort($arr){$len=count($arr);//获取要排序数组的长度for($i=0;$i<$len;$i++){//外层循环遍历整个数组for($j=0;$j<$len-$i-1;$j++){//内层循环用于比较相邻元素,次数随外层循环进行而减少if(......
  • 代码随想录算法训练营第五天| 242.有效的字母异位词 , 349. 两个数组的交集 , 202. 快
    242.有效的字母异位词 繁冗版:1,思路:先建立两个map,对应两个字符串对应的字符,同时对他们进行计数,如果这两个数字相等,那么就是相等2,代码1boolisAnagram_complicate(strings,stringt)2{3unordered_map<char,int>existedCharBys;45for(autoch......
  • 文心一言 VS 讯飞星火 VS chatgpt (39)-- 算法导论5.4 3题
    三、在生日悖论的分析中,要求各人生日彼此独立是否很重要?或者,是否只要两两成对独立就足够了?证明你的答案。文心一言:在生日悖论的分析中,要求各人生日彼此独立是非常重要的。这是因为,如果各人生日不是独立的,而是存在一定的相关性,那么就会影响到概率的计算,从而影响到生日悖论的分析结......
  • ‘极锐’-一种新的锐化算法
    通过一种新的增强算法对图像进行处理,得到细节增强的锐化效果,分析处理效果,edge清楚,detail细腻,artifact较少,适合多场景图像处理,处理速度较快,能够满足实时化处理。暂时命名为极锐,简单易理解的名字,后续再优化。效果展示原图:中强度锐化效果图:高强度锐化效果图:测试程序matlab测......