概述:
在上一篇中,详解了LRU(最近最少使用)算法,这一篇来讲述LFU(Least Frequently Used),即最不经常使用,也是一种页面置换算法。它的淘汰策略是选择使用频次最少的作为淘汰对象,满足一下几个约束:
实现:
对于每个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
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算法的详解与实现到这里结束