虽然,力扣要求是用时间复杂度 O(1) 来解,但是其它方式我感觉也有必要了解,毕竟是一个由浅到深的过程,自己实现一遍总归是好的。因此,我就把五种求解方式,从简单到复杂,都讲一遍。

LFU实现

力扣原题描述如下:

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value) - 如果键不存在,请设置或插入值。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近 最少使用的键。
「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。示例: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来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lfu-cache

就是要求我们设计一个 LFU 算法,根据访问次数(访问频次)大小来判断应该删除哪个元素,get和put操作都会增加访问频次。当访问频次相等时,就判断哪个元素是最久未使用过的,把它删除。

因此,这道题需要考虑两个方面,一个是访问频次,一个是访问时间的先后顺序。

方案一:使用优先队列

思路:

我们可以使用JDK提供的优先队列 PriorityQueue 来实现 。 因为优先队列内部维护了一个二叉堆,即可以保证每次 poll 元素的时候,都可以根据我们的要求,取出当前所有元素的最大值或是最小值。只需要我们的实体类实现 Comparable 接口就可以了。

当 cache 容量不足时,根据访问频次 freq 的大小来删除最小的 freq 。若相等,则删除 index 最小的,因为index是自增的,越大说明越是最近访问过的,越小说明越是很长时间没访问过的元素。

因本质是用二叉堆实现,故时间复杂度为O(logn)。

public class LFUCache4 {public static void main(String[] args) {LFUCache4 cache = new LFUCache4(2);cache.put(1, 1);cache.put(2, 2);// 返回 1System.out.println(cache.get(1));cache.put(3, 3);    // 去除 key 2// 返回 -1 (未找到key 2)System.out.println(cache.get(2));// 返回 3System.out.println(cache.get(3));cache.put(4, 4);    // 去除 key 1// 返回 -1 (未找到 key 1)System.out.println(cache.get(1));// 返回 3System.out.println(cache.get(3));// 返回 4System.out.println(cache.get(4));}//缓存了所有元素的nodeMap<Integer,Node> cache;//优先队列Queue<Node> queue;//缓存cache 的容量int capacity;//当前缓存的元素个数int size;//全局自增int index = 0;//初始化public LFUCache4(int capacity){this.capacity = capacity;if(capacity > 0){queue = new PriorityQueue<>(capacity);}cache = new HashMap<>();}public int get(int key){Node node = cache.get(key);// node不存在,则返回 -1if(node == null) return -1;//每访问一次,频次和全局index都自增 1node.freq++;node.index = index++;// 每次都重新remove,再offer是为了让优先队列能够对当前Node重排序//不然的话,比较的 freq 和 index 就是不准确的queue.remove(node);queue.offer(node);return node.value;}public void put(int key, int value){//容量0,则直接返回if(capacity == 0) return;Node node = cache.get(key);//如果node存在,则更新它的value值if(node != null){node.value = value;node.freq++;node.index = index++;queue.remove(node);queue.offer(node);}else {//如果cache满了,则从优先队列中取出一个元素,这个元素一定是频次最小,最久未访问过的元素if(size == capacity){cache.remove(queue.poll().key);//取出元素后,size减 1size--;}//否则,说明可以添加元素,于是创建一个新的node,添加到优先队列中Node newNode = new Node(key, value, index++);queue.offer(newNode);cache.put(key,newNode);//同时,size加 1size++;}}//必须实现 Comparable 接口才可用于排序private class Node implements Comparable<Node>{int key;int value;int freq = 1;int index;public Node(){}public Node(int key, int value, int index){this.key = key;this.value = value;this.index = index;}@Overridepublic int compareTo(Node o) {//优先比较频次 freq,频次相同再比较indexint minus = this.freq - o.freq;return minus == 0? this.index - o.index : minus;}}
}

方案二:使用一条双向链表

思路:

只用一条双向链表,来维护频次和时间先后顺序。那么,可以这样想。把频次 freq 小的放前面,频次大的放后面。如果频次相等,就从当前节点往后遍历,直到找到第一个频次比它大的元素,并插入到它前面。(当然,如果遍历到了tail,则插入到tail前面)这样可以保证同频次的元素,最近访问的总是在最后边。

PS:哨兵节点只是为了占位,实际并不存储有效数据,只是为了链表插入和删除时,不用再判断当前节点的位置。不然的话,若当前节点占据了头结点或尾结点的位置,还需要重新赋值头尾节点元素,较麻烦。

为了便于理解新节点如何插入到链表中合适的位置,作图如下:

代码如下:

public class LFUCache {public static void main(String[] args) {LFUCache cache = new LFUCache(2);cache.put(1, 1);cache.put(2, 2);// 返回 1System.out.println(cache.get(1));cache.put(3, 3);    // 去除 key 2// 返回 -1 (未找到key 2)System.out.println(cache.get(2));// 返回 3System.out.println(cache.get(3));cache.put(4, 4);    // 去除 key 1// 返回 -1 (未找到 key 1)System.out.println(cache.get(1));// 返回 3System.out.println(cache.get(3));// 返回 4System.out.println(cache.get(4));}private Map<Integer,Node> cache;private Node head;private Node tail;private int capacity;private int size;public LFUCache(int capacity) {this.capacity = capacity;this.cache = new HashMap<>();/*** 初始化头结点和尾结点,并作为哨兵节点*/head = new Node();tail = new Node();head.next = tail;tail.pre = head;}public int get(int key) {Node node = cache.get(key);if(node == null) return -1;node.freq++;moveToPostion(node);return node.value;}public void put(int key, int value) {if(capacity == 0) return;Node node = cache.get(key);if(node != null){node.value = value;node.freq++;moveToPostion(node);}else{//如果元素满了if(size == capacity){//直接移除最前面的元素,因为这个节点就是频次最小,且最久未访问的节点cache.remove(head.next.key);removeNode(head.next);size--;}Node newNode = new Node(key, value);//把新元素添加进来addNode(newNode);cache.put(key,newNode);size++;}}//只要当前 node 的频次大于等于它后边的节点,就一直向后找,// 直到找到第一个比当前node频次大的节点,或者tail节点,然后插入到它前面private void moveToPostion(Node node){Node nextNode = node.next;//先把当前元素删除removeNode(node);//遍历到符合要求的节点while (node.freq >= nextNode.freq && nextNode != tail){nextNode = nextNode.next;}//把当前元素插入到nextNode前面node.pre = nextNode.pre;node.next = nextNode;nextNode.pre.next = node;nextNode.pre = node;}//添加元素(头插法),并移动到合适的位置private void addNode(Node node){node.pre = head;node.next = head.next;head.next.pre = node;head.next = node;moveToPostion(node);}//移除元素private void removeNode(Node node){node.pre.next = node.next;node.next.pre = node.pre;}class Node {int key;int value;int freq = 1;//当前节点的前一个节点Node pre;//当前节点的后一个节点Node next;public Node(){}public Node(int key ,int value){this.key = key;this.value = value;}}
}

可以看到不管是插入元素还是删除元素时,都不需要额外的判断,这就是设置哨兵节点的好处。

由于每次访问元素的时候,都需要按一定的规则把元素放置到合适的位置,因此,元素需要从前往后一直遍历。所以,时间复杂度 O(n)。

更多干货 请点击查看

LFU的多种实现方式,从简单到复杂,新手必看相关推荐

  1. AP+AC旁挂式组网(简单易懂!新手必看!)

    1.什么是无线组网?         无线组网是指通过无线通信技术,将多个设备连接在一起形成一个网络,实现数据交换和共享资源的过程.它可以帮助用户方便地构建一个覆盖面广.易于扩展的网络,适用于许多场景 ...

  2. 在conda虚拟环境中配置cuda+cudnn+pytorch深度学习环境(新手必看!简单可行!)

    本人最近接触深度学习,想在服务器上配置深度学习的环境,看了很多资料后总结出来了对于新手比较友好的配置流程,创建了一个关于深度学习环境配置的专栏,包括从anaconda到cuda到pytorch的一系列 ...

  3. 简单易用 炒股必看的时序预测基本方法--移动平均(SMA、EMA、WMA)

    来源:TimeSeries 移动平均作为时间序列中最基本的预测方法,计算虽简单但却很实用.不仅可以用于预测,还有一些其他的重要作用,比如平滑序列波动,揭示时间序列的趋势特征. 时间序列预测 移动平均就 ...

  4. 新手必看篇!3种简单的网络数据抓取

    3种抓取其中数据的方法.首先是正则表达式,然后是流行的BeautifulSoup模块,最后是强大的lxml模块. 1 正则表达式 当我们使用正则表达式抓取国家(或地区)面积数据时,首先需要尝试匹配`` ...

  5. 新手必看,HTML和CSS快速简单入门

    html,css静态资源标签简述入门及用法介绍 静态资源: * HTML:用于搭建基础网页,展示页面的内容 * CSS:用于美化页面,布局页面 * JavaScript:控制页面的元素,让页面有一些动 ...

  6. python 创建文件_Python入学首次项目,新手必看,简单易操作

    继昨天文章python软件pycharm安装教程之后,今天则给新手小白们分享一哈,怎么制作并创建文件.print "hello world": 如后期需要资料文件的则可以私信留言, ...

  7. 网站SEO其实很简单,新手必看!!!

    在互联网时代的今天,网站多的数不胜数,企业的官网.个人的论坛博客等,网站SEO也是随着发展趋势水涨船高. 在所有的网站拥有者中,不管是企业主还是个人站长,其中很多人知道SEO,但是更多的人是不知道SE ...

  8. 车内看车头正不正技巧_一看就会的倒车入库技巧!比驾校教的要简单,新手必看_搜狐汽车...

    在现在这个社会中,汽车已经走向了千家万户,基本上每个家庭都会有自己的一辆私家车.由于私家车的增多,路上的车况复杂多样,以及停车难的问题越发增多.开车的人每天都要面临停车难题,特别是新手司机,每天在路上 ...

  9. 亚马逊服务器上传文件是什么类型,新手必看!亚马逊的三种产品上传方式对比...

    作为亚马逊卖家,如何快速.详尽并准确地上传产品是产品畅销与否的第一步,本篇文章为卖家介绍并对比亚马逊站内上传产品的三种方法. 一. 后台手动创建新商品 1.进入亚马逊卖家后台,如下图所示点击" ...

最新文章

  1. 【译】Asp.net MVC 利用自定义RouteHandler来防止图片盗链 (转)
  2. ActiveMQ配置文档
  3. 【更新至2.0】cnbeta 根据评论数提取热喷新闻的js脚本
  4. linux基础:7、基础命令介绍(2)
  5. 中国五大物联网平台优势分析
  6. VUE项目启动:You may use special comments to disable some warnings
  7. 最详细的git( Github和Gitee )入门使用(上传与克隆)
  8. 28.ldconfig
  9. 苹果手机显示iphone已停用连接itunes_iTunes备份道理我都懂,但我依然不想备份的?...
  10. python怎么播放音乐_Python实现在线音乐播放器
  11. 整车控制器(VCU)策略及开发流程
  12. Node.js七天搞定微信公众号(又名:Koa2实现电影微信公众号前后端开发)
  13. 在FL Studio中如何制作白噪音的转场效果
  14. 看柴静《苍穹之下》有感
  15. 【Android Camera2】玩转图像数据 -- NV21图像旋转,镜像,转rgba代码分析,性能优化
  16. 聊一聊什么是SaaS,以及遇到的问题......
  17. ios启动时间优化--理论
  18. lisp正负调换_坐标提取lisp程序 -
  19. 福布斯发布2019年度全球亿万富豪榜:贝佐斯蝉联首富 马化腾马云上榜
  20. Win32 组合框控件的使用

热门文章

  1. stm32单片机屏幕一直闪_stm32实现LED灯的闪烁
  2. js获取当前日期_vue项目中获取前后N天日期
  3. 大脚导入配置选择哪个文件_IntelliJ IDEA 最常用配置(收藏篇)
  4. 2019手机浏览器排名_浏览器排行榜2019年1月浏览器市场份额排名
  5. Github 的 Pull Request 教程
  6. 裸辞后,从Android转战Web前端的学习以及求职之路
  7. Browser Security-同源策略、伪URL的域
  8. js练习 好友列表选择
  9. YII2 Model 类切换数据库连接
  10. github远程提交简单入门