手撕 LFU 缓存

news2024/11/29 0:30:40

大家好,我是 方圆。LFU 的缩写是 Least Frequently Used,简单理解则是将使用最少的元素移除,如果存在多个使用次数最小的元素,那么则需要移除最近不被使用的元素。LFU 缓存在 LeetCode 上是一道困难的题目,实现起来并不容易,所以决定整理和记录一下。如果大家想要找刷题路线的话,可以参考 Github: LeetCode。

LFU 缓存

LeetCode 原题:460. LFU 缓存 困难。

思路:我们需要维护两个 HashMap,分别为 keyNodeMapaccessNodeMap,它们的职责如下:

  • keyNodeMap: key 为 put 的值的 key 值,value 为该 key 对应的节点,那么我们便可以通过这个 map 以 O(1) 的时间复杂度 get 到对应的值

  • accessNodeMap: key 为访问次数,各个节点被 put 和 get 都会使节点 accessNum 访问次数加 1,value 为该访问次数下的 循环双向链表的头节点,通过双向链表我们能以 O(1) 的时间复杂度将节点移除。我们定义 在相同访问次数下,越早插入的节点越靠近双向链表的尾端,在进行节点移除时,会将尾节点移除。

为了更好的理解两个 map 与链表节点的关系,我们用下图对容量为 3 的缓存进行表示,其中绿色代表链表节点,节点中各个值对应的字段为 key, value, accessNum

lfu.png

除此之外,我们要定义一个 minAccessNum 的字段来维护当前缓存中最小的访问次数,这样我们就能够在时间复杂度为 O(1) 的情况下在 accessNodeMap 中获取到对应访问次数的双向链表。

大致的方向确定了,我们需要再想一下具体的实现:

get 方法:我们首先去 keyNodeMap 中拿,没有的话返回 -1 即可。如果有对应的 key 的话,那么我们需要将对应节点的访问次数加 1,并需要改变它所在 accessNodeMap 中的位置:首先需要断开它与原链表的连接,之后加入到新的链表中,如果在 accessNodeMap 中有对应次数的链表,那么我们需要把它插入到该链表的 头节点;如果没有对应访问次数的双向链表的话,我们需要创建该访问次数的链表,并以该节点为头节点,维护在 accessNodeMap 中。这里需要注意,我们要对 minAccessNum 进行 更新,如果该节点的访问次数和 minAccessNum 相等,并且该节点在原来链表删除后,该访问次数下的链表中不存在其他任何节点,那么 minAccessNum 也要加 1。

put 方法:我们同样也需要在 keyNodeMap 中判断是否存在,存在的话需要将值进行覆盖,之后的处理逻辑与 get 方法一致。如果不存在的话,我们这里需要判断缓存的容量 是否足够,足够的话比较简单,先将其 put 到 keyNodeMap 中,再在 accessNodeMap 中将其插入到 key 为 1 的双向链表的头节点即可,这里要注意更改 minAccessNum 为 1,因为新插入的节点一定是访问次数最少的;如果不够的话那么先要 将最少使用的节点移除(在两个 map 中都要移除),在 accessNodeMap 中进行移除时,需要根据 minAccessNum 获取对应的双向链表,移除它的尾节点。在尾节点移除完之后,执行的逻辑和上述容量足够时执行插入节点的逻辑一致。

具体实现已经比较清楚了,直接上代码吧,大家可以关注一下注释信息:

class LFUCache {

    /**
     * 双向链表节点
     */
    static class Node {

        Node left;

        Node right;

        int key;

        int value;

        int accessNum;

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

    }

    private HashMap<Integer, Node> keyNodeMap;

    private HashMap<Integer, Node> accessNodeMap;

    private int minAccessNum;

    private int capacity;

    public LFUCache(int capacity) {
        keyNodeMap = new HashMap<>(capacity);
        accessNodeMap = new HashMap<>(capacity);
        minAccessNum = 0;
        this.capacity = capacity;
    }

    public int get(int key) {
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            // 如果所在链表只有一个节点的话,那么直接将该访问次数的链表删掉
            if (node == node.right) {
                accessNodeMap.remove(node.accessNum);
                // 维护缓存中最小的访问次数
                if (minAccessNum == node.accessNum) {
                    minAccessNum++;
                }
            } else {
                // 断开与原链表的连接
                node.left.right = node.right;
                node.right.left = node.left;
                // 如果该节点是头节点的话,那么需要替换为它的下一个节点作为头节点
                if (node == accessNodeMap.get(node.accessNum)) {
                    accessNodeMap.put(node.accessNum, node.right);
                }
            }

            // 增加后的访问次数链表看看有没有
            node.accessNum++;
            if (accessNodeMap.containsKey(node.accessNum)) {
                Node target = accessNodeMap.get(node.accessNum);
                // 插入头节点
                insertHead(node, target);
            } else {
                // 没有的话,直接 put 即可
                accessNodeMap.put(node.accessNum, node);
                // 单节点循环链表
                node.left = node;
                node.right = node;
            }

            return node.value;
        } else {
            return -1;
        }
    }

    public void put(int key, int value) {
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            node.value = value;
            // 执行get方法
            get(key);
        } else {
            Node node = new Node(key, value, 1);
            if (keyNodeMap.size() == capacity) {
                // 容量不够需要将最少使用的节点移除
                Node oldNodeHead = accessNodeMap.get(minAccessNum);

                Node tail = oldNodeHead.left;
                // 如果所在链表只有一个节点的话,那么直接将该访问次数的链表删掉
                if (tail.right == tail) {
                    accessNodeMap.remove(tail.accessNum);
                } else {
                    // 断开与原链表的连接
                    tail.left.right = tail.right;
                    tail.right.left = tail.left;
                    // 如果该节点是头节点的话,那么需要替换为它的下一个节点作为头节点
                    if (oldNodeHead == accessNodeMap.get(tail.accessNum)) {
                        accessNodeMap.put(tail.accessNum, tail.right);
                    }
                }
                keyNodeMap.remove(tail.key);
            }
            // 这样就有有足够的容量了
            keyNodeMap.put(key, node);

            // 是否有对应的链表
            if (accessNodeMap.containsKey(node.accessNum)) {
                // 插入头节点
                insertHead(node, accessNodeMap.get(node.accessNum));
            } else {
                // 没有对应的链表 直接插入
                accessNodeMap.put(node.accessNum, node);
                node.left = node;
                node.right = node;
            }
            minAccessNum = 1;
        }
    }

    private void insertHead(Node node, Node target) {
        // 拼接到该链表头,并构建循环双向链表
        node.right = target;
        node.left = target.left;
        target.left.right = node;
        target.left = node;
        // 覆盖头节点
        accessNodeMap.put(node.accessNum, node);
    }

}

需要注意的是:

  1. 因为我们维护的是循环双向链表,所以在插入头节点时注意尾节点和头节点的引用关系

  2. 因为我们在 accessNodeMap 中维护的是头节点,所以当我们将链表的头结点进行移除时,需要将头节点的下一个节点作为新的头节点保存在 accessNodeMap

针对第二点我们可以做一个优化,每当第一次生成双向链表的时候,我们创建一个哨兵节点作为头节点,那么这样我们就无需在头节点被移除后再将新的头节点插入 accessNodeMap 中进行覆盖了,始终保持 accessNodeMap 中 value 值保存的是哨兵节点,最终代码如下:

class LFUCache {
    /**
     * 双向链表节点
     */
    static class Node {

        int key, value;

        Node pre, next;

        int accessNum;

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

    /**
     * 记录访问最小的值
     */
    private int minAccessNum;

    private final int capacity;

    private final HashMap<Integer, Node> accessNodeMap;

    private final HashMap<Integer, Node> keyNodeMap;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        accessNodeMap = new HashMap<>(capacity);
        keyNodeMap = new HashMap<>(capacity);

        // 初始化访问次数为 1 的哨兵节点
        minAccessNum = 1;
        accessNodeMap.put(minAccessNum, initialSentinelNode(minAccessNum));
    }

    public int get(int key) {
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            // 找到新的位置
            insertIntoNextSentinel(node);

            return node.value;
        }

        return -1;
    }

    public void put(int key, int value) {
        if (keyNodeMap.containsKey(key)) {
            Node node = keyNodeMap.get(key);
            node.value = value;

            insertIntoNextSentinel(node);
        } else {
            if (keyNodeMap.size() == capacity) {
                // 移除最老的节点
                removeEldest();
            }
            // 新加进来的肯定是最小访问次数 1
            minAccessNum = 1;
            Node newNode = new Node(key, value, minAccessNum);

            // 插入到头节点
            insertIntoHead(newNode, accessNodeMap.get(minAccessNum));
            keyNodeMap.put(key, newNode);
        }
    }

    /**
     * 插入下一个链表中
     */
    private void insertIntoNextSentinel(Node node) {
        // 在原来的位置移除
        remove(node);
        // 尝试更新 minAccessNum
        tryToIncreaseMinAccessNum(node.accessNum++);
        // 获取增加 1 后的哨兵节点
        Node nextCacheSentinel = getSpecificAccessNumSentinel(node.accessNum);
        // 插入该链表的头节点
        insertIntoHead(node, nextCacheSentinel);
    }

    /**
     * 在原链表中移除
     */
    private void remove(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
        node.next = null;
        node.pre = null;
    }

    /**
     * 尝试更新 minAccessNum
     */
    private void tryToIncreaseMinAccessNum(int accessNum) {
        // 原访问次数的哨兵节点
        Node originSentinel = accessNodeMap.get(accessNum);
        // 如果只剩下哨兵节点的话,需要看看需不需要把 minAccessNum 增加 1
        if (originSentinel.next == originSentinel && originSentinel.accessNum == minAccessNum) {
            minAccessNum++;
        }
    }

    /**
     * 获取指定访问次数的哨兵节点
     */
    private Node getSpecificAccessNumSentinel(int accessNum) {
        if (accessNodeMap.containsKey(accessNum)) {
            return accessNodeMap.get(accessNum);
        } else {
            // 没有的话得初始化一个
            Node nextCacheSentinel = initialSentinelNode(accessNum);
            accessNodeMap.put(accessNum, nextCacheSentinel);

            return nextCacheSentinel;
        }
    }

    /**
     * 生成具体访问次数的哨兵节点
     */
    private Node initialSentinelNode(int accessNum) {
        Node sentinel = new Node(-1, -1, accessNum);
        // 双向循环链表
        sentinel.next = sentinel;
        sentinel.pre = sentinel;

        return sentinel;
    }

    /**
     * 插入头节点
     */
    private void insertIntoHead(Node node, Node nextCacheSentinel) {
        node.next = nextCacheSentinel.next;
        nextCacheSentinel.next.pre = node;
        nextCacheSentinel.next = node;
        node.pre = nextCacheSentinel;
    }

    /**
     * 如果容量满了的话,需要把 minAccessNum 访问次数的尾巴节点先移除掉
     */
    private void removeEldest() {
        Node minSentinel = accessNodeMap.get(minAccessNum);

        Node tail = minSentinel.pre;
        tail.pre.next = tail.next;
        minSentinel.pre = tail.pre;
        keyNodeMap.remove(tail.key);
    }
}

巨人的肩膀

  • LFU 缓存官方题解

  • 【宫水三叶】运用「桶排序」&「双向链表」实现 LFUCache

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1028280.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Fair下发产物-布局DSL生成原理

一、概述 大家都知道,Flutter在release环境是以AOT模式运行的,这就决定了我们要做动态化的话无法简单的通过动态下发dart代码执行的。根据Fair团队的前期调研,我们对布局动态化和逻辑动态化的实现采用了两套不同的实现方案,对于布局部分,我们在解析dart源文件之后生成DSL…

xterm使用

xterm使用 前言1. xterm介绍2. xterm使用2.1 xterm简单示例2.2 xterm监听输入并在终端中实时显示方式1:onKey监听方式2:onData监听onData和onKey什么区别 2.3 xterm与vue整合2.3 xterm vue websocket 附录配置说明 前言 vue与xterm整合记录 1. xterm介绍 xterm 是一个基于…

jvm深入研究文档--java中的堆--详解!--jvm底层探索(1)

阿丹&#xff1a; JVM的内存分区包括以下几个部分&#xff1a; 堆区&#xff08;Heap&#xff09; - 这是JVM的主要部分&#xff0c;用于存储实例对象和大多数Java对象&#xff0c;如数组和用户定义的类。方法区&#xff08;Method Area&#xff09; - 这是线程私有的&#x…

内网穿透的应用-NAS私有云存储 - 搭建Nextcloud私有云盘并公网远程访问

文章目录 摘要1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访问 4 配置固定http公网地址4.1 保留一个二级子域名4.1 配置固定二级子域名4.3 测试访问公网固定二级子域名 摘要 Nextcloud,它是ownCloud的一个分支,是一个文件共享服…

利用fiddler正向代理前端请求到本地后端

前景&#xff1a;在实际开发测试环境中&#xff0c;&#xff08;前后端均已上线到测试服务器或前端以上线而后端还在开发中)。在测试过程中&#xff08;前端页面点击&#xff0c;功能测试&#xff09;发现了bug或异常点。 正常排查问题可能是先利用浏览器检查工具查看接口的返回…

Unity Bolt UGUI事件注册方式总结

Bolt插件提供了丰富的事件注册方式&#xff0c;开发者几乎不用编写任何代码就可以完成事件的注册&#xff0c;进行交互。下面是我使用UI事件注册的相关总结。 1、通过UI控件自身拖拽实现事件的注册。 Button的事件注册&#xff1a; 新建一个UnityEvent事件&#xff0c; Butt…

PASCAL VOC2012数据集详细介绍

PASCAL VOC2012数据集详细介绍 0、数据集介绍2、Pascal VOC数据集目标类别3、 数据集下载与目录结构4、目标检测任务5、语义分割任务6、实例分割任务7、类别索引与名称对应关系 0、数据集介绍 2、Pascal VOC数据集目标类别 在Pascal VOC数据集中主要包含20个目标类别&#xff…

uniapp开发h5 调用微信sdk 全网最全指南!!!! 血泪史!!!

目录 场景&#xff1a; 技术栈&#xff1a; 遇到的问题先抛出来&#xff1a; 1.通过后端同学获取调用微信sdk所需的签名过程中&#xff0c;遇到的跨域问题 2.使用微信sdk前提必须是微信容器&#xff0c;换句话说就是微信浏览器打开&#xff0c;才能使用微信sdk 3.如何在开…

方案:浅析利用AI智能识别与视频监控技术打造智慧水产养殖监管系统

一、方案背景 针对目前水产养殖集约、高产、高效、生态、安全的发展需求&#xff0c;基于智能传感、智慧物联网、人工智能、视频监控等技术打造智慧水产系统&#xff0c;成为当前行业的发展趋势。传统的人工观察水产养殖方式较为单一&#xff0c;难以及时发现人员非法入侵、偷…

Windows系统如何部署Wing FTP Server与公网远程访问【内网穿透】

Wing FTP Server安装配置结合内网穿透实现公网访问本地站点 文章目录 Wing FTP Server安装配置结合内网穿透实现公网访问本地站点前言1.Wing FTP Server下载安装2.Wing FTP Server配置部署3.安装cpolar内网穿透3.1 注册账号3.2 下载cpolar客户端3.3 登录cpolar web ui管理界面3…

ChatGLM 实现一个BERT

前言 本文包含大量源码和讲解,通过段落和横线分割了各个模块,同时网站配备了侧边栏,帮助大家在各个小节中快速跳转,希望大家阅读完能对BERT有深刻的了解。同时建议通过pycharm、vscode等工具对bert源码进行单步调试,调试到对应的模块再对比看本章节的讲解。 涉及到的jupyt…

【多目标跟踪】 TrackFormer 耗时三天 单句翻译!!!

【多目标跟踪】 TrackFormer 耗时三天 单句翻译&#xff01;&#xff01;&#xff01; TrackFormer: Multi-Object Tracking with Transformers Abstract The challenging task of multi-object tracking (MOT) re-quires simultaneous reasoning about track initiali…

纽禄美卡Neuromeka亮相美国FABTECH,展示用于焊接的3D视觉协作机器人

原创 | 文 BFT机器人 纽禄美卡Neuromeka公司在由美国精密成型协会、美国焊接协会、化工涂料协会等5大协会举办的美国金属加工及焊接展览会FABTECH上精彩亮相。这家总部位于韩国首尔的公司成立于2013年&#xff0c;是机器人解决方案领域的领先供应商&#xff0c;致力于提高各种…

通过 chatgpt 协助完成网站数据破解

Chatgpt 的出现极大地提升了程序员的工作效率&#xff0c;常见的使用场景包括代码自动生成、代码静态检查等&#xff0c;那么 chatgpt 能否用于某些网站的数据破解工作呢&#xff1f; 问题 某天线上服务开始报警&#xff0c;原来是某个视频网站无法获取到其 cdn 地址导致的下…

【AD】【规则设置】设置四层板

设置四层板 一般 4层板&#xff0c;都会把 地 和 VCC放在内层。1、使用快捷键D-K 进入层叠管理器&#xff0c;添加负片层添加完后&#xff0c;修改层名&#xff0c;方便辨识修改格式&#xff1a;属性层号 2、进入相应layer 设置网络设置GND层设置VCC层特点&#xff1a;在层内可…

【HackTheBox Topology】打靶记录

一、信息收集 1、nmap 扫描发现22 80 端口 2、访问80端口 找到两个域名 topology.htb latex.topology.htb 3、子域扫描发现如下两个域名 dev.topology.htb stats.topology.htb C:\root> gobuster vhost -u http://topology.htb --append-domain -w /usr/share/seclists…

【Android Framework系列】第15章 Fragment+ViewPager与Viewpager2相关原理

1 前言 上一章节【Android Framework系列】第14章 Fragment核心原理(AndroidX版本&#xff09;我们学习了Fragment的核心原理&#xff0c;本章节学习常用的FragmentViewPager以及FragmentViewPager2的相关使用和一些基本的源码分析。 2 FragmentViewPager 我们常用的两个Page…

【基本数据结构 四】线性数据结构:队列

学习了栈后,再来看看第四种线性表结构,也就是队列,队列和栈一样也是一种受限的线性表结构,和栈后进先出的操作方式不同的是,队列是FIFO的结构,也就是先进先出的操作方式。 队列的定义 队列这个概念非常好理解。可以把它想象成排队买票,先来的先买,后来的人只能站末尾…

iTOP-RK3568开发板Linux 修改kernel logo

本文档配套资料在网盘资料“iTOP-3568 开发板\02_【iTOP-RK3568 开发板】开发资料\10_Linux 系统开发配套资料\05_Linux 修改内核 logo 配套资料”路径下。 5.3.1 准备 logo 系统默认内核 logo&#xff0c;如下图所示&#xff1a; 如 果 想 要 替 换 这 个 logo, 首 先 要 制…

SAP服务器文件管理

SAP服务器文件管理 文件说明&#xff1a;对于SAP服务器的文件管理&#xff0c;系统给出3个事物码&#xff0c;分别是显示目录的AL11&#xff0c;下载文件的 CG3Y和上传文件的CG3Z。 AL11显示目录:以查找系统参数文件为例&#xff0c;在前台执行事物码AL11进入&#xff0c;如图…