LFU 的设计与实现

news2025/1/13 10:16:28

LFU 的设计与实现

作者:Grey

原文地址:

博客园:LFU 的设计与实现

CSDN:LFU 的设计与实现

题目描述

LFU(least frequently used)。即最不经常使用页置换算法。

题目链接:LeetCode 460. LFU Cache

主要思路

首先,定义一个辅助数据结构 Node

    public static class Node {
      public Integer key;
      public Integer value;
      public Integer times; // 这个节点发生get或者set的次数总和
      public Node up; // 节点之间是双向链表所以有上一个节点
      public Node down; // 节点之间是双向链表所以有下一个节点

      public Node(int k, int v, int t) {
        key = k;
        value = v;
        times = t;
      }
    }

这个 Node 用于封装 LFU Cache 每次加入的元素,其中 key 和 value 两个变量记录每次加入的 KV 值,times 用于记录该 KV 值被操作(get/set)的次数之和, up 和 down 两个变量用于链接和 KV 出现词频一样的数据项,用链表串联。

接下来需要另外一个辅助数据结构 NodeList,前面的 Node 结构已经把词频一致的数据项组织在同一个桶里,这个 NodeList 用于连接出现不同词频的桶,用双向链表组织

    public static class NodeList {
      public Node head; // 桶的头节点
      public Node tail; // 桶的尾节点
      public NodeList last; // 桶之间是双向链表所以有前一个桶
      public NodeList next; // 桶之间是双向链表所以有后一个桶

      public NodeList(Node node) {
        head = node;
        tail = node;
      }
      ……
    }

使用一个具体的示例来表示上述两个结构如何组织的

例如,LFU Cache 在初始为空的状态下,进来如下数据

key = A, value = 3

key = B, value = 30

key = C, value = 4

key = D, value = 12

那么 LFU 会做如下组织

img
此时只有出现一次的桶,接下来,如果 key = C 这条记录 被访问过了,所以词频变为2,接下来要把 key = C 这条记录先从词频为1的桶里面取出来,然后再新建一个词频为 2 的桶,把这个 key = C 的数据项挂上去,结果如下

img

接下来,如果又操作了 key = C 这条记录,那么这条记录的词频就是 3, 又需要新增一个词频为 3 的桶,原来词频为 2 的桶已经没有数据项了,要销毁,并且把词频为 1 的桶和词频为 3 的桶连接在一起。

img

接下来,如果操作了 key = A,则 key = A 成为词频为 2 的数据项,再次新增词频为 2 的桶,并把这个桶插入到词频为 1 和词频为 3 的桶之间,如下图

img

以上示例就可以很清楚说明了 Node 和 NodeList 两个数据结构在 LFU 中的作用,接下来,为了实现快速的 put 和 get 操作,需要定义如下成员变量

int capacity; // 缓存的大小限制
int size; // 缓存目前有多少个节点
HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表
HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
NodeList headList; // 整个结构中位于最左的桶,是一个双向链表

说明:records 这个变量就是用于快速得到某个 key 的节点(Node)是什么,由于这里的 kv 都是整型,所以用 Integer 作为 key 可以定位到对应的 Node 数据项信息。

heads 则用于快速定位某个 Node 在哪个桶里面。

headList 表示整个结构中位于最左侧的桶,这个桶一定是出现次数最少的桶,所以淘汰的时候,优先淘汰这个桶里面的末尾位置,即 tail 位置的 node!

两个核心方法 put 和 get 的核心代码说明如下

    public void put(int key, int value) {
      
      if (records.containsKey(key)) {
// put 的元素是已经存在的
// 更新元素值,更新出现次数
        Node node = records.get(key);
        node.value = value;
        node.times++;
        // 通过heads以O(1)复杂度定位到所在的桶
        NodeList curNodeList = heads.get(node);
        // 把这个更新后的 Node 从 旧的桶迁移到新的桶
        move(node, curNodeList);
      } else {
        if (size == capacity) {
            // 容量已经满了
            // 淘汰 headList 尾部的节点!因为这个节点是最久且最少用过的节点
          Node node = headList.tail;
          headList.deleteNode(node);
          // 删掉的节点有可能会让 headList 换头,因为最右侧的桶可能只有一个节点,被删除后,就没有了。
          modifyHeadList(headList);
          // records和 heads 中都要删掉其记录
          records.remove(node.key);
          heads.remove(node);
          size--;
        }
        // 以上操作就是淘汰了一个节点
        // 接下来就放心加入节点
        // 先建立Node,词频设置为 1
        Node node = new Node(key, value, 1);
        if (headList == null) {
            // 如果headList为空,说明最左侧的桶没有了,新来节点正好充当最左侧节点的桶中元素
          headList = new NodeList(node);
        } else {
          if (headList.head.times.equals(node.times)) {
            // 最右侧桶不为空的情况下,这个节点出现的次数又正好等于最左侧桶所代表的节点数
            // 则直接加入最左侧桶中
            headList.addNodeFromHead(node);
          } else {
            // 将加入的节点作为做左侧桶,接上原先的headList
            // eg:新加入的节点出现的次数是1,原先的 headList代表的桶是词频为2的数据
            // 就会走这个分支
            NodeList newList = new NodeList(node);
            newList.next = headList;
            headList.last = newList;
            headList = newList;
          }
        }
        records.put(key, node);
        heads.put(node, headList);
        size++;
      }
    }

    public int get(int key) {
      if (!records.containsKey(key)) {
        // 不包含这个key
        // 按题目要求直接返回 -1
        return -1;
      }
      // 否则,先取出这个节点
      Node node = records.get(key);
      // 词频+1
      node.times++;
      // 将这个节点所在的桶找到
      NodeList curNodeList = heads.get(node);
      // 将这个节点从原桶调整到新桶
      move(node, curNodeList);
      return node.value;
    }

PS:这里涉及的对双向链表和桶链表的两个操作movemodifyHeadList逻辑不难,但是很多繁琐的边界条件要处理,具体方法的说明见上述代码注释,不赘述。

完整代码如下

static class LFUCache {

    private int capacity; // 缓存的大小限制
    private int size; // 缓存目前有多少个节点
    private HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表
    private HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
    private NodeList headList; // 整个结构中位于最左的桶

    public LFUCache(int capacity) {
      this.capacity = capacity;
      size = 0;
      records = new HashMap<>();
      heads = new HashMap<>();
      headList = null;
    }

    // 节点的数据结构
    public static class Node {
      public Integer key;
      public Integer value;
      public Integer times; // 这个节点发生get或者set的次数总和
      public Node up; // 节点之间是双向链表所以有上一个节点
      public Node down; // 节点之间是双向链表所以有下一个节点

      public Node(int k, int v, int t) {
        key = k;
        value = v;
        times = t;
      }
    }

    // 桶结构
    public static class NodeList {
      public Node head; // 桶的头节点
      public Node tail; // 桶的尾节点
      public NodeList last; // 桶之间是双向链表所以有前一个桶
      public NodeList next; // 桶之间是双向链表所以有后一个桶

      public NodeList(Node node) {
        head = node;
        tail = node;
      }

      // 把一个新的节点加入这个桶,新的节点都放在顶端变成新的头部
      public void addNodeFromHead(Node newHead) {
        newHead.down = head;
        head.up = newHead;
        head = newHead;
      }

      // 判断这个桶是不是空的
      public boolean isEmpty() {
        return head == null;
      }

      // 删除node节点并保证node的上下环境重新连接
      public void deleteNode(Node node) {
        if (head == tail) {
          head = null;
          tail = null;
        } else {
          if (node == head) {
            head = node.down;
            head.up = null;
          } else if (node == tail) {
            tail = node.up;
            tail.down = null;
          } else {
            node.up.down = node.down;
            node.down.up = node.up;
          }
        }
        node.up = null;
        node.down = null;
      }
    }
    private boolean modifyHeadList(NodeList removeNodeList) {
      if (removeNodeList.isEmpty()) {
        if (headList == removeNodeList) {
          headList = removeNodeList.next;
          if (headList != null) {
            headList.last = null;
          }
        } else {
          removeNodeList.last.next = removeNodeList.next;
          if (removeNodeList.next != null) {
            removeNodeList.next.last = removeNodeList.last;
          }
        }
        return true;
      }
      return false;
    }


    private void move(Node node, NodeList oldNodeList) {
      oldNodeList.deleteNode(node);
      NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;
      NodeList nextList = oldNodeList.next;
      if (nextList == null) {
        NodeList newList = new NodeList(node);
        if (preList != null) {
          preList.next = newList;
        }
        newList.last = preList;
        if (headList == null) {
          headList = newList;
        }
        heads.put(node, newList);
      } else {
        if (nextList.head.times.equals(node.times)) {
          nextList.addNodeFromHead(node);
          heads.put(node, nextList);
        } else {
          NodeList newList = new NodeList(node);
          if (preList != null) {
            preList.next = newList;
          }
          newList.last = preList;
          newList.next = nextList;
          nextList.last = newList;
          if (headList == nextList) {
            headList = newList;
          }
          heads.put(node, newList);
        }
      }
    }

    public void put(int key, int value) {
      if (capacity == 0) {
        return;
      }
      if (records.containsKey(key)) {
        Node node = records.get(key);
        node.value = value;
        node.times++;
        NodeList curNodeList = heads.get(node);
        move(node, curNodeList);
      } else {
        if (size == capacity) {
          Node node = headList.tail;
          headList.deleteNode(node);
          modifyHeadList(headList);
          records.remove(node.key);
          heads.remove(node);
          size--;
        }
        Node node = new Node(key, value, 1);
        if (headList == null) {
          headList = new NodeList(node);
        } else {
          if (headList.head.times.equals(node.times)) {
            headList.addNodeFromHead(node);
          } else {
            NodeList newList = new NodeList(node);
            newList.next = headList;
            headList.last = newList;
            headList = newList;
          }
        }
        records.put(key, node);
        heads.put(node, headList);
        size++;
      }
    }

    public int get(int key) {
      if (!records.containsKey(key)) {
        return -1;
      }
      Node node = records.get(key);
      node.times++;
      NodeList curNodeList = heads.get(node);
      move(node, curNodeList);
      return node.value;
    }
  }

更多

算法和数据结构笔记

参考资料

算法和数据结构体系班-左程云

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

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

相关文章

浅析Linux字符设备驱动程序内核机制

前段时间在学习linux设备驱动的时候&#xff0c;看了陈学松著的《深入Linux设备驱动程序内核机制》一书。说实话&#xff0c;这是一本很好的书&#xff0c;作者不但给出了在设备驱动程序开发过程中的所需要的知识点&#xff08;如相应的函数和数据结构&#xff09;&#xff0c;…

从零开始的MySQL(2)

目录1.数据库约束1.1 unique1.2 not null1.3 default1.5 primary key1.6 foreign key2. 将A的记录插入到B中3.聚合函数3.1 count()函数3.2 sum()函数3.3 avg()函数3.4 max()函数3.5 MIN()函数3.6 group by4.联合查询4.1 内连接4.2 外连接4.3 自连接4.4 子连接1.数据库约束 约束…

【简单易上手】昇思MindSpore邀你定制专属Diffusion模型

昇思MindSpore首个可训练diffusion模型DDPM马上要和大家见面了&#xff0c;操作简单&#xff0c;可训练推理&#xff0c;单卡即可运行&#xff0c;欢迎广大产学研开发者使用启智社区免费Ascend NPU算力体验 最近爆火的AI绘图&#xff0c;相信大家并不陌生了。 从AI绘图软件生成…

知识图谱的知识表示:向量表示方法

目录 从词向量到实体向量 知识图谱向量表示学习模型 TransE TransH TransR TransD TransX系列总结 DisMult 从词向量到实体向量 知识图谱的向量表示。有一类词是代表实体的&#xff0c;假如对这类实体词的向量做一些计算&#xff0c;比如用Rome向量减去Italy的向量&am…

使用界面组件Telerik ThemeBuilder研发主题,只需要这七步!

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供最完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序。 ThemeBuilder是一个web应用程序&#xff0…

前端基础(一)_初识JavaScript

最开始的时候&#xff0c;浏览器只能显示文本和图片&#xff0c;并不能做各种动态的操作。 一、JavaScript的历史 1、网景公司的布兰登艾奇开发了js 2、借鉴了java的数据管理机制、c的函数 3、Js的创建是用来解决浏览器和用户之间互动的问题&#xff08;表单提交的问题&#…

LeanCloud 快速实现服务端

1. 实现与服务器交互平台 1.1 Google 平台的 Firebase (需要科学网络) Firebasehttps://firebase.google.cn/ 1.2 LeanCloud 平台 LeanCloudhttps://www.leancloud.cn/ 2. 配置信息 2.1 在 LeanCloud 控制台创建应用, 根据 SDK下载 开发指南配置应用 2.2 配置文件 build…

拉伯杠杆平台|新冠药龙头跳水,万亿产业开启新阶段!

依照此前发表的时间表&#xff0c;新修订的《体育法》将于2023年1月1日起正式施行。 12月27日早盘&#xff0c;A股前期大热的抗疫体裁呈现显着回落。熊去氧胆酸、新冠药、肝炎概念、新冠检测、生物疫苗等体裁跌幅较大。熊去氧胆酸、新冠药两大板块跌幅超过4%&#xff0c;抢手龙…

外贸采购管理对业务的影响及解决方案

在外贸企业中&#xff0c;采购环节的把控对整个业务环节都有影响。像是采购供应商是否能够按时到货&#xff0c;会直接影响生产进度&#xff1b;采购质量的好坏直接影响产品的生产进度及质量&#xff1b;采购成本的高低&#xff0c;直接影响产品的成本及利润&#xff1b;采购供…

NSUM 问题

目录标题两数之和两数之和 2&#xff08;多个结果集去重&#xff09;15. 三数之和两数之和 问题描述 给定一个整数数组 nums 和一个目标值 target&#xff0c;请你在该数组中找出和为目标值的那 两个 整数&#xff0c;并返回他们的数组下标。 你可以假设每种输入只会对应一个答…

基于Java(JSP)+MySQL实现(Web)学生成绩综合管理系统【100010065】

Java 课程设计:师生交流系统 1.1 课设题目 信息商务学院学生成绩综合管理系统的综合与开发 2.1 课设目的 《Java 程序设计》课程是计算机专业的一门专业必修课程&#xff0c;针对课程进行为期两周的实践教学&#xff0c;旨在提升本专业学生对此课程中的理论知识的综合应用能…

[Python学习系列] 走进Django框架

文章目录1. 安装django2. 创建项目&#xff08;命令的方式&#xff09;在第一次创建django项目需要做的&#xff1a;每一次创建django项目需要做的&#xff1a;3. 创建app4. 启动运行django项目程序5. 模板&静态文件的存放地址6. django中的模板语法7. django中常见的请求和…

ArcGIS基础实验操作100例--实验4矢量要素的镜像复制,缩放

实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验4 矢量要素的镜像复制&#xff0c;缩放 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&#xff09;加载【Mirror Features】工具 &#x…

VMware之安装Windows10系统

系统下载 下载地址&#xff1a;原版软件 (itellyou.cn) 创建虚拟机系统框架 在菜单栏中选择文件下的新建虚拟机 选择自定义&#xff0c;然后点击下一步 直接下一步 选择稍后安装操作系统&#xff0c;然后点击下一步 因为安装的系统是Windows&#xff0c;这里注意选择Microsof…

使用eBPF追踪Linux内核

简介 BPF&#xff0c;及伯克利包过滤器Berkeley Packet Filter&#xff0c;最初构想提出于 1992 年&#xff0c;其目的是为了提供一种过滤包的方法&#xff0c;并且要避免从内核空间到用户空间的无用的数据包复制行为。它最初是由从用户空间注入到内核的一个简单的字节码构成&…

浏览器:理解HTTP无状态与Cookie的使用

一、理解HTTP无状态 1.1、理解http无状态 http无状态是指协议对于用户身份、用户状态、用户权限、交互场景等没有记忆能力。简单讲就是不能识别用户。 1.2、http无状态的优点&#xff1a; 可以更快地处理大量的事务&#xff0c;确保协议的可伸缩性&#xff0c;减少服务器的…

一文搞定十大排序算法(动画图解)

排序的定义 排序&#xff0c;就是重新排列表中的元素&#xff0c;使表中的元素满足按关键字递增或递减的过程。为了査找方便&#xff0c;通常要求计算机中的表是按关键字有序的。 排序的确切定义如下&#xff1a; 算法的稳定性&#xff1a; 若待排序表中有两个元素 Ri 和 Rj&am…

包装类和简单认识泛型

目录 1.包装类 1.1基本数据类型和对应的包装类 1.2装箱和拆箱 1.3自动装箱和自动拆箱 2.什么是泛型 3.引出泛型 3.1语法 4.泛型类的使用 4.1语法 4.2类型推导 5.裸类型 5.1说明 6.泛型如何编译的 6.1擦除机制 6.2为什么不能实例化泛型类型的数组 7.泛型的上界 …

MaxKey单点登录认证系统3.5.12发布,重要漏洞修复

业界领先的IAM/IDaas身份管理和认证产品 概述 MaxKey单点登录认证系统&#xff0c;谐音马克思的钥匙寓意是最大钥匙,是业界领先的IAM/IDaas身份管理和认证产品,支持OAuth 2.x/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议&#xff0c;提供安全、标准和开放的用户身份…

STM32/51单片机实训day3——点亮LED灯、闪烁LED灯(二)实践

内 容&#xff1a;编写代码实现LED灯的点亮功能 学 时&#xff1a;2学时 知识点&#xff1a;分析原理图、LED灯控制原理 重点&#xff1a;GPIO参数配置、LED原理图分析 难点&#xff1a;编写 GPIO参数配置函数、LED点亮函数 时间&#xff1a;2022年12月21日 9:00&#xff5e;…