概述:
在上一篇中,详解了LRU(最近最少使用)算法,这一篇来讲述LFU(Least Frequently Used),即最不经常使用,也是一种页面置换算法。它的淘汰策略是选择使用频次最少的作为淘汰对象,满足一下几个约束:
- 使用get(K key)方法获取值时,其使用频次加一
- 使用put(K key, V value)方法添加时,如果key在缓存中已存在,则更新对应的value,并且其使用频次加一;否则判断缓存是否已满,是则移除缓存中使用频次最少的key,若有多个key的使用频次最少,则移除最旧的那个key;然后把新的key添加到缓存中,其对应的使用频次置为1。
下面来介绍需要使用怎样的数据结构来满足上述约束。
实现:
对于每个key需要保存对应的value和使用频次frequency,为此可以将这个两个封装起来,定义一个类Pair,
private class Pair {
V value;
int frequency;
public Pair(V value, int frequency) {
this.value = value;
this.frequency = frequency;
}
}
使用K-V键值对这种map结构可以使得get和put的时间复杂度为O(1),即定义
Map<K, Pair> keyMap;
通过keyMap可以快速的获取key对应的value和frequency;
怎么知道哪个key的frequency最小呢?把每个key拿出来依次比较?显然是不合理的,我们可以在全局范围内维护一个最小频次minFrequency;
上面约束的第二点说到,minFrequency可能对应多个key,这些key要怎么弄存储?我们需要再定义一个Map<K, V>用来存frequency对应的key集合,这里很显然K对应frequency,用Integer类就好,V用什么类型呢?当要删除minFrequency对应的key时,若key有多个则删除最旧的,也就是要满足先进先出原则,同事又要满足删除key的这个操作的时间复杂度为O(1),那就用队列+set这种数据结构把,即LinkedHashSet,最终Map定义为
Map<Integer, LinkedHashSet<K>> frequencyMap;
get方法的实现,
public V get(K key) {
if (!keyMap.containsKey(key)) {
return null;
}
// 每访问一次key,对应的频次加一
increaseFrequency(key);
return keyMap.get(key).value;
}
put方法的实现,
public void put(K key, V value) {
if (this.capacity < 1) {
return;
}
if (keyMap.containsKey(key)) {
keyMap.get(key).value = value;
increaseFrequency(key);
return;
}
// 如果容量满了,先剔除最近最少使用的key
if (this.capacity == keyMap.size()) {
removeMinFrequency();
}
// 更新
this.minFrequency = 1;
keyMap.put(key, new Pair(value, this.minFrequency));
frequencyMap.putIfAbsent(this.minFrequency, new LinkedHashSet<>());
frequencyMap.get(this.minFrequency).add(key);
}
这两个方法逻辑都比较清晰,就不详细讲解了,下面主要来看看频次加一的方法increaseFrequency(K key),
private void increaseFrequency(K key) {
// 频次加一之前,先获取key的频次
int frequency = keyMap.get(key).frequency;
// 从该frequency对应的key集合中剔除key
LinkedHashSet<K> keys = frequencyMap.get(frequency);
keys.remove(key);
if (keys.isEmpty()) {
frequencyMap.remove(frequency);
// 若剔除的频次刚好等于全局的最小频次,则要更新minFrequency
// 因为该minFrequency已没有对应的key了
if (frequency == this.minFrequency) {
this.minFrequency++;
}
}
// 频次加一,更新keyMap与frequencyMap
frequency += 1;
keyMap.get(key).frequency = frequency;
frequencyMap.putIfAbsent(frequency, new LinkedHashSet<>());
frequencyMap.get(frequency).add(key);
}
还有删除最不经常使用的key方法removeMinFrequency(),
private void removeMinFrequency() {
// 根据全局的最小频次minFrequency获得对应的key集合
LinkedHashSet<K> keys = frequencyMap.get(this.minFrequency);
// key是按顺序添加的,因此需要移除最先添加的,即顺序遍历的第一个
K key = keys.iterator().next();
keys.remove(key);
if (keys.isEmpty()) {
frequencyMap.remove(this.minFrequency);
// 这里因为调用本方法的前提是容量满了,
// 而每新添加一个key,它对应的频次是1,minFrequency也会置为1
// 故这里不需要更新minFrequency,在put方法中已更新
}
// 接着再从keyMap里移除key
keyMap.remove(key);
}
最后是完整代码,
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
public class LFUCache<K, V> {
int capacity;
int minFrequency;
Map<K, Pair> keyMap;
Map<Integer, LinkedHashSet<K>> frequencyMap;
public LFUCache(int capacity) {
this.capacity = capacity;
keyMap = new HashMap<>();
frequencyMap = new HashMap<>();
}
public V get(K key) {
if (!keyMap.containsKey(key)) {
return null;
}
// 每访问一次key,对应的频次加一
increaseFrequency(key);
return keyMap.get(key).value;
}
public void put(K key, V value) {
if (this.capacity < 1) {
return;
}
if (keyMap.containsKey(key)) {
keyMap.get(key).value = value;
increaseFrequency(key);
return;
}
// 如果容量满了,先剔除最近最少使用的key
if (this.capacity == keyMap.size()) {
removeMinFrequency();
}
// 更新
this.minFrequency = 1;
keyMap.put(key, new Pair(value, this.minFrequency));
frequencyMap.putIfAbsent(this.minFrequency, new LinkedHashSet<>());
frequencyMap.get(this.minFrequency).add(key);
}
private void increaseFrequency(K key) {
// 频次加一之前,先获取key的频次
int frequency = keyMap.get(key).frequency;
// 从该frequency对应的key集合中剔除key
LinkedHashSet<K> keys = frequencyMap.get(frequency);
keys.remove(key);
if (keys.isEmpty()) {
frequencyMap.remove(frequency);
// 若剔除的频次刚好等于全局的最小频次,则要更新minFrequency
// 因为该minFrequency已没有对应的key了
if (frequency == this.minFrequency) {
this.minFrequency++;
}
}
// 频次加一,更新keyMap与frequencyMap
frequency += 1;
keyMap.get(key).frequency = frequency;
frequencyMap.putIfAbsent(frequency, new LinkedHashSet<>());
frequencyMap.get(frequency).add(key);
}
private void removeMinFrequency() {
// 根据全局的最小频次minFrequency获得对应的key集合
LinkedHashSet<K> keys = frequencyMap.get(this.minFrequency);
// key是按顺序添加的,因此需要移除最先添加的,即顺序遍历的第一个
K key = keys.iterator().next();
keys.remove(key);
if (keys.isEmpty()) {
frequencyMap.remove(this.minFrequency);
// 这里因为调用本方法的前提是容量满了,
// 而每新添加一个key,它对应的频次是1,minFrequency也会置为1
// 故这里不需要更新minFrequency,在put方法中已更新
}
// 接着再从keyMap里移除key
keyMap.remove(key);
}
private class Pair {
V value;
int frequency;
public Pair(V value, int frequency) {
this.value = value;
this.frequency = frequency;
}
}
}
至此,LFU算法的详解与实现到这里结束