今天我们通过java语言手动实现LFU算法策略及原理分析,这是redis缓存策略的一种,因此了解次算法及原很有必要;另外,leetcode 也有次题,介绍是这样的:
/** * 请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。 * * 实现 LFUCache 类: * * LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 * int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。 * void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时, * 则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。 * 为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。 * * 当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。 * * 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 * * 示例: * * 输入: * ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] * [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]] * 输出: * [null, null, null, 1, null, -1, 3, null, -1, 3, 4] * * 解释: * // cnt(x) = 键 x 的使用计数 * // cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的) * LFUCache lfu = new LFUCache(2); * lfu.put(1, 1); // cache=[1,_], cnt(1)=1 * lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1 * lfu.get(1); // 返回 1 * // cache=[1,2], cnt(2)=1, cnt(1)=2 * lfu.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小 * // cache=[3,1], cnt(3)=1, cnt(1)=2 * lfu.get(2); // 返回 -1(未找到) * lfu.get(3); // 返回 3 * // cache=[3,1], cnt(3)=2, cnt(1)=2 * lfu.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用 * // cache=[4,3], cnt(4)=1, cnt(3)=2 * lfu.get(1); // 返回 -1(未找到) * lfu.get(3); // 返回 3 * // cache=[3,4], cnt(4)=1, cnt(3)=3 * lfu.get(4); // 返回 4 * // cache=[3,4], cnt(4)=2, cnt(3)=3 * * * 提示: * * 0 <= capacity <= 104 * 0 <= key <= 105 * 0 <= value <= 109 * 最多调用 2 * 105 次 get 和 put 方法 */
下面我们开始分析实现:
1、LFU是什么
LFU是一种淘汰算法(最近最少使用),全称是Least Frequently Used.
2、、LFU算法的思想
算法的思想就是:如果一个数据在最近一段时间内访问次数很少,
那么在将来它被访问的可能性也很小。
所以,当指定的空间已存满数据时,应当把访问次数很少的数据淘汰。
这里可以看出LRU和LFU的区别一个是访问数据的顺序,一个是数据的访问次数
3、LFU实现
根据算法思想实现需要满足已下条件:
1)新增key(默认访问次数1),当空间已存满数据,删除访问次数最少时间最长的key。
2)获取值时快速找到某个key是否存在,存在key(访问次数加+1),并返回其对应的 value。
3)多个key可能具有相同的访问次数
4、原理图:
5、存储原理:
6、核心实现代码:
import java.util.HashMap;
public class LFUCache {
public static void main(String[] args) {
LFUCache lfuCacheTask = new LFUCache(2);
lfuCacheTask.put(1,1);
lfuCacheTask.put(2,2);
lfuCacheTask.get(1);
lfuCacheTask.put(3,3);
lfuCacheTask.get(2);
lfuCacheTask.get(3);
lfuCacheTask.put(4,4);
lfuCacheTask.get(1);
lfuCacheTask.get(3);
lfuCacheTask.get(4);
}
MyLFUCache<Integer,Integer> myLFUCache;
public LFUCache(Integer capacity){
myLFUCache = new MyLFUCache<>(capacity);
}
public int get(Integer key){
Node<Integer, Integer> node = myLFUCache.get(key);
return node == null ? -1 : node.value;
}
public void put(Integer key,Integer value){
myLFUCache.set(key,value);
}
public static class Node<K,V>{
private K key;
private V value;
private Integer mapKey=1;//此节点被操作的次数
private Node<K,V> next;
private Node<K,V> last;
public Node() {}
public Node(K key,V value){
this.key = key;
this.value = value;
}
}
public static class DoubleNodeList<K,V>{
private Node head;
private Node tail;
/**
* 双向链表初始化
*/
public DoubleNodeList(){
head = new Node();
tail = new Node();
head.next = tail;
tail.last = head;
}
/**
* 删除最后一个节点
*/
private void removeLastNode(){
if(tail.last == head){
return;
}
removeNode(tail.last);
}
/**
* 删除双向链表的某个节点(首、尾、或者其他节点通用)
* @param node
* @return
*/
private void removeNode(Node<K,V> node){
node.last.next = node.next;
node.next.last = node.last;
}
/**
* 添加到尾节点
* @param newNode
*/
private void addToTail(Node<K,V> newNode){
newNode.next = tail;
newNode.last = tail.last;
tail.last.next = newNode;
tail.last = newNode;
}
/**
* 同理,在某个节点前面添加一个节点也是如此逻辑
* @param current
* @param newNode
*/
private void addBeforeNode(Node<K,V> current,Node<K,V> newNode){
newNode.last = current.last;
newNode.next = current;
current.last.next = newNode;
current.last = newNode;
}
/**
* 添加到首节点
* @param newNode
*/
private void addToHead(Node<K,V> newNode){
newNode.next = head.next;
newNode.last = head;
head.next.last = newNode;
head.last = newNode;
}
/**
* 同理,在某个节点后面添加一个节点也是如此逻辑
* @param current
* @param newNode
*/
private void addAfterNode(Node<K,V> current,Node<K,V> newNode){
newNode.next = current.next;
newNode.last = current;
current.next.last = newNode;
current.next = newNode;
}
}
public static class MyLFUCache<K,V>{
private HashMap<K,Node<K,V>> cacheMap;
private HashMap<Integer,DoubleNodeList<K,V>> incMap;
// private DoubleNodeList<K,V> nodeList;
private final int capacity;
int min;
public MyLFUCache(int capacity) {
cacheMap = new HashMap<>();
incMap = new HashMap<>();
// nodeList = new DoubleNodeList<>();
this.capacity = capacity;
}
public void set(K key,V value){
if (capacity == 0) {
return;
}
Node<K, V> kvNode = cacheMap.get(key);
if(kvNode != null){//已经添加过次节点数据
kvNode.value = value;
freInc(kvNode);
}else {//首次添加
if(cacheMap.size() == capacity){//如果达到容量阈值,需要先删除,单位时间内使用的次数最少的节点数据
DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(min);
cacheMap.remove(kvDoubleNodeList.head.next.key);
//删除首节点,即最长时间没有用到的(单位时间内操作次数相同的情况下)
kvDoubleNodeList.removeNode(kvDoubleNodeList.head.next);
}
Node<K,V> newNode = new Node<>(key,value);
cacheMap.put(key,newNode);
//查询并创建首次操作的记录
DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(1);
if(kvDoubleNodeList == null){
kvDoubleNodeList = new DoubleNodeList<>();
incMap.put(1,kvDoubleNodeList);
}
//添加到尾节点,即最近时间用到的数据
kvDoubleNodeList.addToTail(newNode);
min = 1;
}
}
public Node<K, V> get(K key){
Node<K, V> kvNode = cacheMap.get(key);
if (kvNode == null) {
return null;
}
freInc(kvNode);
return kvNode;
}
private void freInc(Node<K,V> kvNode) {
Integer mapKey = kvNode.mapKey;//找到单位时间内操作的次数
DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(mapKey);//找到改此次对应的所有的节点数据
kvDoubleNodeList.removeNode(kvNode);//删除改节点,后面会加到新的 mapKey+1 对应的链表里
//判断次链表里是否还有数据
if(min == mapKey && kvDoubleNodeList.head.next == kvDoubleNodeList.tail){
min++;
}
mapKey++;
kvNode.mapKey++;
kvDoubleNodeList = incMap.get(mapKey);//先找,再存
if(kvDoubleNodeList == null){
kvDoubleNodeList = new DoubleNodeList<>();
incMap.put(mapKey,kvDoubleNodeList);
}
kvDoubleNodeList.addToTail(kvNode);
}
}
}
测试代码注释比较全,多多测试思考,定会很快掌握。