https://leetcode-cn.com/problems/lfu-cache/description/

缓存的实现可以采取多种策略,不同策略优点的评估就是“命中率”。好的策略可以实现较高的命中率。常用的策略如:LRU(最近最少使用)、LFU(最不频繁使用)。这两种策略都可以在O(1)时间内实现get和put。关于LRU,在 http://www.cnblogs.com/weiyinfu/p/8546080.html 中已经介绍过。本文主要讲讲LFU的实现。

LFU比LRU复杂,为什么这么说呢?当每个元素只访问一次,则各个元素的使用频率都是1,这是遵循的法则是LRU,即越早被访问的元素越先被删除。LRU的实现可以用Java中的LinkedHashSet实现。

这里复习一下三种Set的区别和联系:

  • HashSet:哈希集,元素无序,读写O(1)
  • TreeSet:元素有序,读写都是O(lgN)
  • LinkedHashSet:双向链表+哈希集,元素有序,元素的顺序为插入的顺序,读写复杂度O(1)

方法一:使用LinkedHashSet实现LRU

第一种方法:三个哈希,使用HashSet实现LRU,因为HashSet中的元素使用的是Integer,可以在HashSet上直接实现LRU;如果HashSet中的元素使用的是Node,则无法直接从HashSet中删除元素。

LFU的关键思路:

  • 对于新插入的元素,它的使用频率是1。如果缓存满了,必须在插入新元素之前移除掉旧元素而不能在插入新元素之后移除最低频使用的元素,因为那样可能会把刚刚插入的新元素删掉。
  • 只需要一个min记录当前使用频次最低的元素,如果新元素来之前队列满了,肯定要删除掉这个min元素,而不是其它使用频次较高的元素。即便这个min元素以后使用频次超过了“倒数第二”,在超过之前一定可以遇到“倒数第二”。
  • LFU需要LRU作为桶,盛放那些使用频次相同的元素。

这段程序的技巧性在于只使用Integer而不使用自定义类型。

import java.util.HashMap;
import java.util.LinkedHashSet; class LFUCache { public int capacity;//容量大小
public HashMap<Integer, Integer> map = new HashMap<>();//存储put进去的key和value
public HashMap<Integer, Integer> frequent = new HashMap<>();//存储每个key的频率值
//存储每个频率的相应的key的值的集合,这里用HashSet是因为其是由HashMap底层实现的,可以O(1)时间复杂度查找元素
//而且linked是有序的,同一频率值越往后越最近访问
public HashMap<Integer, LinkedHashSet<Integer>> list = new HashMap<>();
int min = -1;//标记当前频率中的最小值 public LFUCache(int capacity) {
this.capacity = capacity;
} public int get(int key) {
if(!map.containsKey(key)){
return -1;
}else{
int value = map.get(key);//获取元素的value值
int count = frequent.get(key);
frequent.put(key, count + 1); list.get(count).remove(key);//先移除当前key //更改min的值
if(count == min && list.get(count).size() == 0)
min++; LinkedHashSet<Integer> set = list.containsKey(count + 1) ? list.get(count + 1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(count + 1, set); return value;
} } public void put(int key, int value) {
if(capacity <= 0){
return;
}
//这一块跟get的逻辑一样
if(map.containsKey(key)){
map.put(key, value);
int count = frequent.get(key);
frequent.put(key, count + 1); list.get(count).remove(key);//先移除当前key //更改min的值
if (count == min && list.get(count).size() == 0)
min++; LinkedHashSet<Integer> set = list.containsKey(count + 1) ? list.get(count + 1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(count + 1, set);
}else{
if(map.size() >= capacity){
Integer removeKey = list.get(min).iterator().next();
list.get(min).remove(removeKey);
map.remove(removeKey);
frequent.remove(removeKey);
}
map.put(key, value);
frequent.put(key, 1);
LinkedHashSet<Integer> set = list.containsKey(1) ? list.get(1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(1, set); min = 1;
} } public static void main(String[] args) {
LFUCache lfuCache = new LFUCache(2);
lfuCache.put(2, 1);
lfuCache.put(3, 2);
System.out.println(lfuCache.get(3));
System.out.println(lfuCache.get(2));
lfuCache.put(4, 3);
System.out.println(lfuCache.get(2));
System.out.println(lfuCache.get(3));
System.out.println(lfuCache.get(4));
}
}

方法二:使用LinkedHashMap实现LRU

方法其实跟方法一是一样的,方法一使用LinkedHashSet+HashMap实现LRU,实际上完全可以改为LinkedHashMap<Integer,Integer>,这样就能够使用两个组件:frequencyMapHashMap<frequency,LRU>来实现LFU。

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; class LFUCache {
//key出现的频率为value
HashMap<Integer, Integer> frequency = new HashMap<>();
//频率为key的hashMap为value
HashMap<Integer, LinkedHashMap<Integer, Integer>> a = new HashMap<>();
//时刻记住需要更新哪些全局变量
int min = 0;//最小频率
int capacity;//容器的容量
int nowsize = 0;//当前容器中元素个数 public LFUCache(int capacity) {
this.capacity = capacity;
} public String tos(Map<Integer, Integer> ma) {
StringBuilder builder = new StringBuilder();
for (int i : ma.keySet()) {
builder.append(i + ":" + ma.get(i) + " ");
}
return builder.toString();
} public void debug() {
System.out.println(tos(frequency));
for (int i : a.keySet()) {
System.out.println(i + " " + tos(a.get(i)));
}
System.out.println("======");
} public int get(int key) {
Integer f = frequency.get(key);
if (f == null) {
return -1;
}
int value = a.get(f).get(key);
active(key);//激活一下key,使其频率+1
return value;
} void active(int key) {
int f = frequency.get(key);
frequency.put(key, f + 1);
LinkedHashMap<Integer, Integer> src = a.get(f), des = a.getOrDefault(f + 1, new LinkedHashMap<>());
des.put(key, src.remove(key));
tryRemove(f);
a.put(f + 1, des);
} void tryRemove(int frequency) {
if (a.get(frequency).size() == 0) {
if (frequency == min) {
min++;
}
a.remove(frequency);
}
} void removeLFU() {
LinkedHashMap<Integer, Integer> ma = a.get(min);
int removing = ma.keySet().iterator().next();
ma.remove(removing);//移除掉最早插入的那个结点
tryRemove(min);
frequency.remove(removing);
nowsize--;
} public void put(int key, int value) {
if (capacity == 0) return;
if (frequency.get(key) == null) {
if (capacity == nowsize) removeLFU();
nowsize++;
frequency.put(key, 1);
LinkedHashMap<Integer, Integer> ff = a.getOrDefault(1, new LinkedHashMap<>());
ff.put(key, value);
a.put(1, ff);
min = 1;//新插入结点之后,最低频率必然为1
} else {
active(key);
a.get(frequency.get(key)).put(key, value);
}
} public static void main(String[] args) {
LFUCache cache = new LFUCache(2);
String[] op = {"put", "put", "get", "put", "get", "get", "put", "get", "get", "get"};
int[][] value = {{1, 1}, {2, 2}, {1}, {3, 3}, {2}, {3}, {4, 4}, {1}, {3}, {4}};
for (int i = 0; i < op.length; i++) {
System.out.println(op[i] + " " + value[i] + " " + cache.min);
cache.debug();
if (op[i].equals("put")) {
cache.put(value[i][0], value[i][1]);
} else {
cache.get(value[i][0]);
}
}
}
} /**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/

方法三:最佳复杂度

在上面的方法中,重要缺点之一就是空间复杂度略微有点高,因为每一个LRU都是使用HashMap实现的,而每一个频率对应一个LRU,这就导致当使用的频率种数很多时,HashMap很多,造成空间巨大浪费。

LFU跟LRU思路是一样的,把最近使用过的东西从左往右排成一排(右面的频率比较高),当使用一个元素之后,把这个元素频率加1,向右面移动几格。应该移动到什么地方呢?这需要快速定位,所以需要快速找到每个频率的最后一个元素,这可以通过建立一个频率到结点的映射来实现。


import java.util.HashMap;
import java.util.Map; class LFUCache {
//定义双向链表的结点
class Node {
Node prev, next;
int key, value;
int frequency; Node(int key, int value) {
this.key = key;
this.value = value;
} @Override
public String toString() {
return "(" + key + ":" + value + " " + frequency + ")";
}
} //定义双向链表
class LinkedList {
Node head, tail; LinkedList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
} //移除双向链表中的结点
void remove(Node node) {
Node prev = node.prev;
Node next = node.next;
prev.next = next;
next.prev = prev;
} //在who之后插入newNode
void insertAfter(Node who, Node newNode) {
Node next = who.next;
who.next = newNode;
newNode.next = next;
next.prev = newNode;
newNode.prev = who;
} @Override
public String toString() {
StringBuilder builder = new StringBuilder();
for (Node i = head.next; i != tail; i = i.next) {
builder.append(String.format("(%d:%d,%d)->", i.key, i.value, i.frequency));
}
return builder.toString();
}
} //缓存的容量
int capacity;
//双向链表
LinkedList link = new LinkedList();
//key到Node的映射
Map<Integer, Node> ma = new HashMap<>();
//频率到尾节点的映射
Map<Integer, Node> tail = new HashMap<>();
int nowsize = 0; public LFUCache(int capacity) {
this.capacity = capacity;
link.head.frequency = 0;
link.tail.frequency = Integer.MAX_VALUE;
tail.put(link.head.frequency, link.head);
tail.put(link.tail.frequency, link.tail);
} String tos(Map<Integer, Node> ma) {
StringBuilder builder = new StringBuilder();
for (int i : ma.keySet()) {
builder.append(i + ":" + ma.get(i) + " ");
}
return builder.toString();
} void debug() {
System.out.println(link.toString());
System.out.println(tos(tail));
System.out.println(tos(ma));
System.out.println("========");
} public int get(int key) {
Node node = ma.get(key);
if (node == null) {
return -1;
}
active(node);//命中,激活之
return node.value;
} void active(Node node) {
int f = node.frequency;
node.frequency++;
Node prev = node.prev;
Node master = tail.get(f);//当前频率的老大
Node masterNext = master.next;//当前老大的下一个
if (node == master) {
if (prev.frequency == f) {//我是老大,后继有人
tail.put(f, prev);
} else {//我是老大,后继无人
tail.remove(f);
}
if (masterNext.frequency == f + 1) {//下一组频率相邻
link.remove(node);
link.insertAfter(tail.get(f + 1), node);
tail.put(f + 1, node);
} else {//下一组频率不相邻,链表结构不变
tail.put(f + 1, node);
}
} else {//我不是老大
if (masterNext.frequency == f + 1) {//下一组频率相邻
link.remove(node);
link.insertAfter(tail.get(f + 1), node);
tail.put(f + 1, node);
} else {//下一组频率不相邻
link.remove(node);
link.insertAfter(master, node);
tail.put(f + 1, node);
}
}
} //移除掉最近最少使用的结点
void removeLFU() {
Node node = link.head.next;
Node next = node.next;
link.remove(node);
ma.remove(node.key);
if (node.frequency != next.frequency) {
tail.remove(node.frequency);
}
} public void put(int key, int value) {
if (capacity == 0) return;
Node node = ma.get(key);
if (node == null) {
if (nowsize >= capacity) {//容量超了,移除LFU
removeLFU();
nowsize--;
}
Node newNode = new Node(key, value);
newNode.frequency = 1;
Node oneMaster = tail.get(1);//使用频率为1的group
if (oneMaster == null) {
link.insertAfter(link.head, newNode);
} else {
link.insertAfter(tail.get(1), newNode);
}
nowsize++;
tail.put(1, newNode);
ma.put(key, newNode);
} else {
active(node);
node.value = value;
}
} public static void main(String[] args) {
LFUCache cache = new LFUCache(3 /* capacity (缓存容量) */);
String[] ops = {"put", "put", "put", "put", "get"};
int[][] values = {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {4}};
for (int i = 0; i < ops.length; i++) {
System.out.println(ops[i] + " " + values[i][0]);
if (ops[i].equals("put")) {
cache.put(values[i][0], values[i][1]);
} else {
int res = cache.get(values[i][0]);
System.out.println(res);
}
cache.debug();
}
}
} /**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/

参考资料

http://www.cnblogs.com/DarrenChan/p/8819996.html

LFU缓存的更多相关文章

  1. -实现 LFU 缓存算法

    -实现 LFU 缓存算法, 设计一个类 LFUCache,实现下面三个函数 + 构造函数: 传入 Cache 内最多能存储的 key 的数量 + get(key):如果 Cache 中存在该 key, ...

  2. 算法进阶面试题06——实现LFU缓存算法、计算带括号的公式、介绍和实现跳表结构

    接着第四课的内容,主要讲LFU.表达式计算和跳表 第一题 上一题实现了LRU缓存算法,LFU也是一个著名的缓存算法 自行了解之后实现LFU中的set 和 get 要求:两个方法的时间复杂度都为O(1) ...

  3. [LeetCode]460.LFU缓存机制

    设计并实现最不经常使用(LFU)缓存的数据结构.它应该支持以下操作:get 和 put. get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1.put(key, valu ...

  4. 详解三种缓存过期策略LFU,FIFO,LRU(附带实现代码)

    在学操作系统的时候,就会接触到缓存调度算法,缓存页面调度算法:先分配一定的页面空间,使用页面的时候首先去查询空间是否有该页面的缓存,如果有的话直接拿出来,如果没有的话先查询,如果页面空间没有满的时候, ...

  5. 【转】缓存淘汰算法系列之2——LFU类

    原文地址 :http://www.360doc.com/content/13/0805/16/13247663_304916783.shtml 1. LFU类 1.1. LFU 1.1.1. 原理 L ...

  6. 缓存淘汰算法之LFU

    1. LFU类 1.1. LFU 1.1.1. 原理 LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频 ...

  7. Go -- LFU类(缓存淘汰算法)(转)

    1. LFU类 1.1. LFU 1.1.1. 原理 LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频 ...

  8. 常用缓存淘汰算法(LFU、LRU、ARC、FIFO、MRU)

    缓存算法是指令的一个明细表,用于决定缓存系统中哪些数据应该被删去. 常见类型包括LFU.LRU.ARC.FIFO.MRU. 最不经常使用算法(LFU): 这个缓存算法使用一个计数器来记录条目被访问的频 ...

  9. 缓存淘汰算法 LRU 和 LFU

    LRU (Least Recently Used), 即最近最少使用用算法,是一种常见的 Cache 页面置换算法,有利于提高 Cache 命中率. LRU 的算法思想:对于每个页面,记录该页面自上一 ...

随机推荐

  1. Intent 常用场景 FileProvider 拍照 裁剪 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  2. Everything 使用技巧 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. spring boot整合mybatis+mybatis-plus

    Spring boot对于我来说是一个刚接触的新东西,学习过程中,发现这东西还是很容易上手的,Spring boot没配置时会默认使用Spring data jpa,这东西可以说一个极简洁的工具,可是 ...

  4. 中文分词 coreseek安装笔记

    #!/bin/bash # create by lhb # date 2013-11-26 # coreseek install script apt-get install make gcc g++ ...

  5. Idea 自动导入包的*设置99

    作者:Intopass 链接:https://www.zhihu.com/question/35806024/answer/64530300 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权, ...

  6. c#:判断一个数组元素中否有重复元素

    给定一个数组,判定该数组中是否有重复元素. 判定该数组中是否有重复元素总结出以下实现方案: using System; using System.Collections.Generic; using ...

  7. [Functional Programming] Create Reusable Functions with Partial Application in JavaScript

    This lesson teaches you how arguments passed to a curried function allow us to store data in closure ...

  8. windows下用qemu搭建android

    1.下载Qemu for windows 版本为qemu-0.9.0-windows 2.下载qemuwith-kqemu-support 安装kqemu的目的就是为了加快qemu的子系统运行速度.在 ...

  9. DELL平板如何安装WIN10系统 -标记活动分区的问题

    在计算机管理中没有这个选项   可以在分区助手软件中,选中C分区之后,左边有设置活动分区,然后左上角提交执行即可        

  10. Java开发 - 异常 - 使用throws

    如果一个方法可能会产生异常,我们需要用throws关键字给它标注会抛出什么异常, 这样就可以在方法调用的时候捕获它. 代码如下: package corejava8.exceptions; publi ...