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. POJ 1265 pick定理

    pick公式:多边形的面积=多边形边上的格点数目/2+多边形内部的格点数目-1. 多边形边上的格点数目可以枚举每条边求出.如果是水平或者垂直,显然可以得到,否则则是坐标差的最大公约数减1.(注这里是不 ...

  2. Kaggle网站流量预测任务第一名解决方案:从模型到代码详解时序预测

    Kaggle网站流量预测任务第一名解决方案:从模型到代码详解时序预测 2017年12月13日 17:39:11 机器之心V 阅读数:5931   近日,Artur Suilin 等人发布了 Kaggl ...

  3. 【转】以太网帧、IP报文格式

    原文:https://www.cnblogs.com/yongren1zu/p/6274460.html https://blog.csdn.net/gufachongyang02/article/d ...

  4. 整数对A满足二叉查找树,B满足最大堆

    1 题目 给出一组整数对 { (a[0], b[0]), (a[1], b[1]) ... (a[n-1], b[n-1]) },全部 a 值和 b 值分别不反复(随意 i != j 满足 a[i] ...

  5. Jquery Ajax 返回数据类型变成document

    下面是我写的一段Jquery Ajax的代码,在chrome下没有问题,在firefox下就算是返回success也提示"系统正忙"; $.ajax({ url: "fa ...

  6. DexHunter脱壳神器分析

    0x00 这篇文章我们分析Android脱壳神器DexHunter的源码. DexHunter作者也写了一篇介绍它的文章从Android执行时出发.打造我们的脱壳神器.DexHunter源码位于htt ...

  7. python 微信企业号

    python 微信企业号 准备,如果没有微信企业号,可以先申请体验号记下CorpID和Secret(获取Token用) 发送消息首先可以在微信的开发者中心,查看接口文档 下面就是python代码:1. ...

  8. Android开发——Android M(6.0) 权限解决方案

    Android开发--Android M(6.0) 权限解决方案 自从Android M(6.0)发布以来,权限管理相比以前有了很大的改变,很多程序员发现之前运行的好好的Android应用在Andro ...

  9. unity3d与web网页通信

    总结一下: Unity3D 中的 C# 和 JavaScript 脚本之间是可以互相访问并交互的,但是要求这些被访问和操作的 C# 和 JavaScript 组件必须放在名为 Standard Ass ...

  10. Android Webservices 返回多行多列数据(Dataset)

    对于之前从事.net或者java开发人员,习惯了从后台获取网格数据(多行多列DataTable),但转行从事android开发,难免会不习惯 Android调用Webservice时,如果返回值是一个 ...