概述:
在上一篇中,详解了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),即定义
MapkeyMap;
通过keyMap可以快速的获取key对应的value和frequency;
怎么知道哪个key的frequency最小呢?把每个key拿出来依次比较?显然是不合理的,我们可以在全局范围内维护一个最小频次minFrequency;
上面约束的第二点说到,minFrequency可能对应多个key,这些key要怎么弄存储?我们需要再定义一个Map
Map> 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 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 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{ int capacity; int minFrequency; Map keyMap; Map > 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 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 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算法的详解与实现到这里结束



