算法leetcode 彻底搞懂LRU和LFU缓存问题

手撕LRU和LFU缓存淘汰算法 LFU()算法是一种用于缓存替换的算法,其思想是淘汰最少被使用的缓存数据。它的基本原理是根据每个数据块的使用频率来决定其是否被淘汰。LFU算法主要用于高速缓存中,以保证缓存空间的高效利用。LFU算法的实现需要一个计数器来记录每个数据块被使用的次数,当缓存空间满了时,选择使用频率最低的数据块进行替换。如果多个数据块的使用频率相同,则选择最早使用的数据块进行替换。LFU算法的优点在于它能够较好地适应不同访问模式下的缓存需求,并且能够保证缓存空间的高效利用。 阅读详情

简介

LRU (Least Recently Used) 缓存,即在缓存容量达到上限的条件下,删除最久未使用过的数据,腾出空间存储新的数据。

LFU (Least Frequently Used) 缓存,即在缓存容量达到上限的条件下,删除使用频率最少的数据,当使用频率相同时,删除最久未使用的数据。

这两种缓存算法在实践中用得非常普遍,例如MySQL的内存数据页淘汰算法,Redis的淘汰策略等。

例题

以上题目均来自leetcode.

详细

146. LRU缓存机制

题目描述

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

分析

这道题有很多实现方式,我们直接看进阶要求,要求 put 和 get 操作要在 O(1) 时间复杂度下完成。

我们可以用一个双向链表存储数据,这样方便移除最久未使用的数据,同时,用一个字典存储 key 和链表中节点的映射关系,方便直接从 key 获取节点,从而进行删除节点等操作。

同时,每次put或者get数据,将数据移动到链表尾部。

代码

lass LRUCache {

    class Node {
        private int key;
        private int value;
        private Node next;
        private Node pre;

        public Node() {
        }

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }

    }

    private int capacity;
    private int count;
    private Map<Integer, Node> map;
    private Node head;
    private Node tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        count = 0;
        map = new HashMap<>();
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.pre = head;
    }
	
	// 移动节点到链表尾部
    private void moveToTail(Node node) {
        // 取出
        node.pre.next = node.next;
        node.next.pre = node.pre;
        // 插入尾部
        addNode(node);
    }
	
	// 在尾部添加一个节点
    private void addNode(Node node) {
        Node pre = tail.pre;
        node.pre = pre;
        pre.next = node;
        node.next = tail;
        tail.pre = node;
    }
	
	// 删除第一个节点
    private void deleteFirst() {
        Node node = head.next;
        node.pre.next = node.next;
        node.next.pre = node.pre;
        map.remove(node.key);
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node node = map.get(key);
        moveToTail(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            moveToTail(node);
        } else {
            Node node = new Node(key, value);
            if (count < capacity) {
                count++;
                map.put(key, node);
                addNode(node);
            } else {
                deleteFirst();
                map.put(key, node);
                addNode(node);
            }
        }
    }
}

说明:在双向链表中,创建虚拟的头结点和尾节点,这样保证链表始终不为空,并且添加进入的数据不会是头结点和尾节点,这样可以省去很多空值的判断。

460. LFU缓存

简介

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。

get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除最久未使用的键。
「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

进阶:
你是否可以在 O(1) 时间复杂度内执行两项操作?

示例:

LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

分析

LFU 和 LRU 的区别在于需要保存每条数据的使用频率。

我们为每个频率保存一个双向链表,同时建立一个频率和相应链表的映射freqMap。

建立另一个keyMap,记录存储数据的 key和对应节点的映射。

代码

class LFUCache {

    class Node {
        private int key;
        private int value;
        private int freq;
        private Node next;
        private Node pre;

        public Node() {
        }

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
            this.freq = 1;
        }
    }

    // 不同频率对应的链表头节点和尾节点
    private Map<Integer, Node[]> freqMap;
    // 键对应的节点
    private Map<Integer, Node> keyMap;
    private int capacity;
    private int count;
    // 记录当前的最小频率
    private int minFreq;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        count = 0;
        freqMap = new HashMap<>();
        keyMap = new HashMap<>();
        minFreq = -1;
    }
	
	// 初始化一个双向链表的首位节点
    private Node[] intiDLinked() {
        Node head = new Node();
        Node tail = new Node();
        head.next = tail;
        tail.pre = head;
        return new Node[]{head, tail};
    }
	
	// 删除一个指定的节点,并修改minFreq
    private void deleteNode(Node node) {
        node.next.pre = node.pre;
        node.pre.next = node.next;
        // check minFreq
        int freq = node.freq;
        Node[] headAndTail = freqMap.get(node.freq);
        if (minFreq == freq && headAndTail[0].next == headAndTail[1]) {
            minFreq++;
        }
    }
	
	// 插入一个节点,根据节点的频率插入到相应的链表中
    private void insertInto(Node node) {
        int freq = node.freq;
        Node tail;
        if (!freqMap.containsKey(freq)) {
            Node[] headAndTail = intiDLinked();
            tail = headAndTail[1];
            freqMap.put(freq, headAndTail);
        } else {
            tail = freqMap.get(freq)[1];
        }
        Node pre = tail.pre;
        pre.next = node;
        node.pre = pre;
        node.next = tail;
        tail.pre = node;
    }
	
	// 将一个节点频率加一后插入新的链表
    private void addFreq(Node node) {
        // get from origin freq
        deleteNode(node);
        node.freq++;
        // insert into new freq
        insertInto(node);
    }
	
	// 淘汰最小频率
    private void deleteMinFreq() {
        Node[] headAndTail = freqMap.get(minFreq);
        Node head = headAndTail[0], tail = headAndTail[1];
        Node node = head.next;
        node.next.pre = node.pre;
        node.pre.next = node.next;
        keyMap.remove(node.key);
        if (head.next == tail) {
            minFreq = 1;  // 此时肯定是有新的数据插入导致需要删除最小频率的数,所以minFreq变为1
        }
    }

    public int get(int key) {
        if (!keyMap.containsKey(key)) {
            return -1;
        }
        Node node = keyMap.get(key);
        addFreq(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (keyMap.containsKey(key)) {
            Node node = keyMap.get(key);
            node.value = value;
            addFreq(node);
        } else {
            Node node = new Node(key, value);
            if (capacity == 0) {
                return;
            } else if (count < capacity) {
                count++;
            } else {
                // 最小频率
                deleteMinFreq();
            }
            node.freq = 1;
            minFreq = 1;
            insertInto(node);
            keyMap.put(key, node);
        }
    }
}

注意:维护一个minFreq,记录当前的最小频率,这样在淘汰元素的时候可以直接去对象的链表删除节点,避免了循环查找最小频率。

LFU缓存 已支持(Java & JS & Python & C) 阅读详情

相关推荐

信号削峰CFR基础知识介绍(一)

接下来,将得到的峰值分成若干段,在每一段中,仅保留该段内所有峰值中幅度最大的峰值。为了减少不必要的带外泄露,削峰后的信号将通过一个低通滤波器,以减少对应于削峰后信号中尖锐拐角的高频信号。首先,它识别出高于由目标PAPR确定的阈值的峰值。在峰值加窗算法中,削峰是通过在峰值区域将原始信号与一个加窗函数相乘来实现的,如下图所示。其中,w(n) 是一种常见的加窗函数,如凯撒窗、高斯窗和汉明窗等,而 p(n) 是一个加权系数。如上图所示,削峰会导致削峰后的信号出现尖锐的拐角,从而引发不必要的带外辐射(ACP增加)。

weixin_49761275的博客 1551

Leetcode.146.LRU缓存机制

设计题:LRU:双向链表+HashMap

qq_43164662的博客 1312

新手别怕!用‘东北天’和‘右前上’坐标系,5分钟搞懂惯性导航姿态矩阵(含Python验证代码)

本文通过‘东北天(ENU)’和‘右前上(RFU)’坐标系,详细解析惯性导航中的姿态矩阵概念。包含Python代码验证,帮助新手快速理解坐标变换、欧拉角及旋转矩阵的构建与应用,特别强调有限转动不可交换性与无限转动次序无关的区别。

weixin_29062255的博客 332

LeetCode解题之十八:LRU

题目 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。 实现 LRUCache 类: LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删

913

Sharding-JDBC 系列专题 - 第九篇:高可用性与集群管理

高可用性是指系统在面对故障(如数据库宕机、网络中断)时,能够快速恢复并继续提供服务。Sharding-JDBC 和 Sharding-Proxy 提供了多种机制支持高可用性,同时需要结合底层数据库的高可用方案(如 MySQL 主从复制、MHA)来实现。高可用性和集群管理是 Sharding-JDBC 和 Sharding-Proxy 在生产环境中成功应用的关键。本篇详细介绍了数据库高可用方案、Sharding-JDBC 的故障切换机制、Sharding-Proxy 的集群部署,以及分布式治理功能。

资深架构师分享:深入剖析软件架构,探讨云计算、微服务、大数据等前沿技术。分享实践经验,促进技术交流,共同构建卓越系统。 1393

EMC案例:解决千兆网口RTL8211F EMI问题

摘要:本文分析了Amlogic S812平台OEM项目中RTL8211F-CG千兆网卡EMI辐射超标问题。发现百兆测试时25MHz时钟有3dB余量,但千兆125MHz时钟导致超标。问题根源包括:未关闭PHY芯片悬空CLKOUT引脚、TX/RXCLK端RC位置错误、包地不完整及参考地缺失。解决方案涉及软件关闭无用时钟、优化RC布局、加强包地与打孔、调整共模电感位置,并在HDMI 3.3V加磁珠。关键结论指出千兆网125MHz时钟需严格包地打孔,且RC取值需平衡EMI与通信性能。

电子绿洲的博客 1278

LeetCode刷题---LRU缓存

如果哈希表中已经存储该节点,则需要对该节点的数据进行更新,首先根据key从哈希表中取出该节点,再对节点中的value重新赋值,之后根据LRU算法将该节点从链表中删除,再添加到链表表头的位置。在写put方法的时候,如果哈希表中未存储该节点,则创建一个新的节点,节点中key和value分别为要添加的键和值,之后将key和新创建的节点添加到哈希表中,在将新的节点添加到链表头部。在写get方法来获取某节点的时候,如果哈希表中存储了该节点,根据LRU算法,则先将该节点删除,再将该节点添加到链表中。

weixin_47109902的博客 2561

从ACTF2020看文件上传漏洞:你以为改后缀就完事了?这些坑我都帮你踩过了

本文通过ACTF2020的Upload题目深入剖析文件上传漏洞的复杂性和防御策略。文章揭示了常见误区,如仅修改文件后缀的局限性,并详细解析了绕过技巧和高级防御机制。从开发角度提供了构建安全上传功能的实用建议,帮助读者全面理解文件上传漏洞的本质与防护方法。

weixin_29278805的博客 246

【LeetCode 146】LRU缓存

今天我们来讲解一道面试高频算法题 LeetCode146题 LRU缓存

小鱼学编程 2489

手把手教你用QEMU模拟AST2600-EVB开发板:OpenBMC环境搭建全流程

本文提供了一份详尽的实战指南,手把手教你使用QEMU模拟器搭建AST2600-EVB开发板的OpenBMC环境。内容涵盖从工具链准备、系统镜像获取(官方与自编译),到QEMU启动命令解析、网络配置(用户与桥接模式),以及通过SSH、WebUI和Redfish API访问服务的全流程,帮助开发者在个人电脑上快速构建安全、可复现的嵌入式开发与测试沙盒。

play7的博客 1132

使用verilog设计实现QR分解

QR(正交三角)分解法是求一般矩阵全部特征值的最有效并广泛应用的方法,一般矩阵先经过正交相似变化成为Hessenberg矩阵,然后再应用QR方法求特征值和特征向量。 使用verilog对QR实现方法进行设计并进行功能仿真: 如下: 顶层模块代码: module TOP( H1R, H1I, H2R, H2I, H3R, H3I, H4R, H4I, clk, rst_n, Y1R, Y1I, Y2R, Y2I, X1R, X1I, X2R, X2I ); parameter width = 16; // I

QQ_778132974的博客 2150

算法编程题-LRU & LFU

对于LRU,认为最长时间没有被访问的元素是应该被淘汰的,而LFU则认为访问次数频率最小的元素应该是要被淘汰的。这里维护最小频率是一个难点,在Get或者Set旧值时,如果当前最小频率等于节点的频率且移除节点后,这种频率对应的链表为空,则说明最小频率需要加一。今年秋招,我有个师兄,本身学历经历表达能力都是拉满的,他简历上有个项目用到的技术里写了LRU,结果好几个大厂的面试都考了这题,这真的是说明了,如果面试官想要你,在算法编程题是不会为难你的,如果不要你,直接给你一道冷门的Hard题。那么思路其实就很明确了。

zhuohuashiyi 855

【DaVinci Developer专题】-8-创建一个枚举Enum ADT(Application Data Type)

目录 1 创建一个枚举Value ADT(Application Data Type) 1.1 命名application data type 1.2 创建一个computation method计算方法属性 1.3 创建一个Contraint约束条件属性 2 创建了一个新的Type Mapping Sets 3 创建了一个S/R Port Interface 4 SWC引用Type Mapping set 5 导入CFG生成代码的总结 6 结尾 <--返回「实战篇」博文-总目录-

「汽车电子助手」的博客 2292

缓存机制 LRU 和 LFU

LRU 是least recently used的缩写,即最近最久未使用。在缓存数据量溢出时,必须淘汰某一项记录才能插入新的记录,而LRU算法则选择淘汰上一次使用时间距离现在最久的那条记录。 数据结构的设计 1、链表list用于保存value,并且从头到尾的顺序达标它最近使用时间(越靠前代表使用越近) 2、哈希表记录key对应的value在链表的位置。 具体实现过程: get: 如果哈希表中存...

hestyle的博客 1123

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

java LFU和LRU实现 (leetcode 460. LFU缓存)

LRU和LFU都是内存管理的页面置换算法。 LFU:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。 LRU:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。 LRU如下: public class LRUCache { class Node { ...

说......的博客 547

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

数据结构之手撕LRU和LFU算法

目录 1.LRU 2.LFU 1.LRU 什么是LRU? LRU就是一种缓存淘汰策略。 计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢? LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该..

qq_56999918的博客 1632

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

LRU & LFU 缓存(LeetCode 146 & 460)

本文详细地介绍了LRU和LFU的底层实现,并提供了对应的代码。实际上,LRU和LFULRU中,对突发性的遍历访问敏感,例如扫描一遍大列表,就很有可能把真正的热点数据挤出去LFU中,初期频率低的热点难以进入长久存在,而长期运行后频率会膨胀,越来越难以淘汰现实会使用改良后的算法,例如W-TinyLFU,结合了滑动窗口、TinyLFU和分段LRU,有兴趣的读者可以自行去了解。

氷泠 757

缓存淘汰算法--LRU算法、LFU、FIFO

目录 一、LRU 1、什么是LRU 2、如何实现 3、分析 4、Java LinkedMap 实现 二、FIFO 三、LFU 一、LRU 1、什么是LRU LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 2、如何实现 最常见的实现是使用一个链表保存缓存...

luoyeyeyu的专栏 828

LRU和LFU算法

图解LRU算法和LFU算法,并附有代码实现。

不积硅步,无以至千里 3613

LRU算法和LFU算法

LRU算法和LFU算法

lxd_max的博客 603

【图解版】力扣第146题:LRU缓存

【图解版】力扣146题:LRU缓存,看不懂来找我!!!

cool-wangtongzhou 1894

缓存淘汰算法 LRU 和 LFU

缓存是一个计算机思维,对于重复的计算,缓存其结果,下次再算这个任务的时候,不去真正的计算,而是直接返回结果,能加快处理速度。当然有些会随时间改变的东西,缓存会失效,得重新计算。 比如缓存空间只有2个,要缓存的数据有很多,1,2,3,4,5,那么当缓存空间满了,需要淘汰一个缓存出去,其中淘汰算法有 LRU,LFU,FIFO,SC二次机会,老化算法,时钟工作集算法等等。 算法流程 LRU,最近最少使用,把数据加入一个链表中,按访问时间排序,发生淘汰的时候,把访问时间最旧的淘汰掉。 比如有数据 1,2,1,

372

【Leetcode】LFU/LRU缓存——自定义双向链表

在做题之前首先对LFU算法有一定的了解。 1. LFU算法 1.1 原理 LFU(Least Frequently Used)算法,即最少访问算法,根据访问缓存的历史频率来淘汰数据,核心思想是“如果数据在过去一段时间被访问的次数很少,那么将来被访问的概率也会很低”。 1.2 数据结构 一般会维护两个数据结构: 哈希:用来提供对外部的访问,查询效率更高; 双向链表或队列:维护了对元素访问次数的排序 缓存操作导致的链表变化: 添加新元素:新元素访问次数为1,放到队尾; 缓存淘汰:从队尾开始淘汰,因为

小朱小朱绝不认输的博客 1332

LRU 与 LFU ,傻傻分不清楚

LRU (Least Recently Used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。 LFU (Least Frequently Used) 最近最少次数使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。 LRU缓存 像浏览器的缓存策略、memcached的缓存策略都是使用LRU这个算法,LRU算法会将近期最不会访问的数据淘汰掉。LRU如此流行的原因是实现比较简单,而且对于实际问题也很实用,良好的运行时性能,命中率较高。 实现 新.

永远相信美好的事情即将发生 3613

LFU算法(Leetcode)

LFU: 采用LFU算法的最简单方法是为每个加载到缓存的快分配一个计数器.每次引用该块时,计数器将增加1.当缓存达到容量并且有一个新块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除 实现: LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序 新加入数据插入到队列尾部(因为引用计数为1) 队列中的数据被访问后,引用计数增加,队列重新排序 当需要淘汰数据时,将已经排序的列表最后的数据块删除 除了LRU,LFU,还有对于LFU算法的改进,比.

you are sherlocked by me! 2098
上一篇: 算法之动态规划总结(11种DP类型,70道全部搞懂)
下一篇: 算法之堆的应用(优先队列)
Elmerr
博客等级 码龄12年 178粉丝 33原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值