其他
字节二面,让写一个LFU缓存策略算法,懵了
大家好,我是D哥
点击关注下方公众号,Java面试资料 都在这里
我们来分析一下LFU:
# LFU是什么
上面这个图就是一个LRU的简单实现思路,在链表的开始插入元素,然后每插入一次计数一次,接着按照次数重新排序链表,如果次数相同的话,按照插入时间排序,然后从链表尾部选择淘汰的数据~
# LRU实现
Node主要包含了key和value,因为LFU的主要实现思想是比较访问的次数,如果在次数相同的情况下需要比较节点的时间,越早放入的越快被淘汰,因此我们需要在Node节点上加入time和count的属性,分别用来记录节点的访问的时间和访问次数。其他的版本实现方式里有新加个内部类来记录 key的count和time,但是我觉得不够方便,还得单独维护一个map,成本有点大。还有一点注意的是这里实现了comparable接口,覆写了compare方法,这里 的主要作用就是在排序的时候需要用到,在compare方法里面我们首先比较节点的访问次数,在访问次数相同的情况下比较节点的访问时间,这里是为了 在排序方法里面通过比较key来选择淘汰的key
/** * 节点 */
public static class Node implements Comparable<Node>{ //键
Object key; //值
Object value; /** * 访问时间 */
long time; /** * 访问次数 */
int count; public Node(Object key, Object value, long time, int count) { this.key = key; this.value = value; this.time = time; this.count = count;
} public Object getKey() { return key;
} public void setKey(Object key) { this.key = key;
} public Object getValue() { return value;
} public void setValue(Object value) { this.value = value;
} public long getTime() { return time;
} public void setTime(long time) { this.time = time;
} public int getCount() { return count;
} public void setCount(int count) { this.count = count;
}
@Override public int compareTo(Node o) { int compare = Integer.compare(this.count, o.count); //在数目相同的情况下 比较时间
if (compare==0){ return Long.compare(this.time,o.time);
} return compare;
}
}
2、定义LFU类
定义LFU类,这里采用了泛型,声明了K和V,还有总容量和一个Map(caches)用来维护所有的节点。在构造方法里将size传递进去,并且创建了一个LinkedHashMap,采用linkedHashMap的主要原因是维护key的顺序
public class LFU<K,V> { /** * 总容量 */
private int capacity; /** * 所有的node节点 */
private Map<K, Node> caches; /** * 构造方法
* @param size */
public LFU(int size) { this.capacity = size;
caches = new LinkedHashMap<K,Node>(size);
}
}
添加元素的逻辑主要是先从缓存中根据key获取节点,如果获取不到,证明是新添加的元素,然后和容量比较,大于预定容量的话,需要找出count计数最小(计数相同的情况下,选择时间最久)的节点,然后移除掉那个。如果在预定的大小之内,就新创建节点,注意这里不能使用 System.currentTimeMillis()方法,因为毫秒级别的粒度无法对插入的时间进行区分,在运行比较快的情况下,只有System.nanoTime()才可以将key的插入时间区分,默认设置count计数为1.如果能获取到,表示是旧的元素,那么就用新值覆盖旧值,计数+1,设置key的time为当前纳秒时间。最后还需要进行排序,这里可以看出插入元素的逻辑主要是添加进入缓存,更新元素的时间和计数~
/** * 添加元素
* @param key
* @param value */
public void put(K key, V value) {
Node node = caches.get(key); //如果新元素
if (node == null) { //如果超过元素容纳量
if (caches.size() >= capacity) { //移除count计数最小的那个key
K leastKey = removeLeastCount();
caches.remove(leastKey);
} //创建新节点
node = new Node(key,value,System.nanoTime(),1);
caches.put(key, node);
}else { //已经存在的元素覆盖旧值
node.value = value;
node.setCount(node.getCount()+1);
node.setTime(System.nanoTime());
}
sort();
}
/** * 排序 */
private void sort() {
List<Map.Entry<K, Node>> list = new ArrayList<>(caches.entrySet());
Collections.sort(list, new Comparator<Map.Entry<K, Node>>() {
@Override public int compare(Map.Entry<K, Node> o1, Map.Entry<K, Node> o2) { return o2.getValue().compareTo(o1.getValue());
}
});
caches.clear(); for (Map.Entry<K, Node> kNodeEntry : list) {
caches.put(kNodeEntry.getKey(),kNodeEntry.getValue());
}
}
淘汰最小的元素这里调用了Collections.min方法,然后通过比较key的compare方法,找到计数最小和时间最长的元素,直接从缓存中移除~
/** * 移除统计数或者时间比较最小的那个
* @return
*/
private K removeLeastCount() {
Collection<Node> values = caches.values();
Node min = Collections.min(values); return (K)min.getKey();
}
获取元素首先是从缓存map中获取,否则返回null,在获取到元素之后需要进行节点的更新,计数+1和刷新节点的时间,根据LFU的原则,在当前时间获取到这个节点以后,这个节点就暂时变成了热点节点,但是它的cout计数也有可能是小于某个节点的count的,所以
此时不能将它直接移动到链表顶,还需要进行一次排序,重组它的位置~
/** * 获取元素
* @param key
* @return
*/
public V get(K key){
Node node = caches.get(key); if (node!=null){
node.setCount(node.getCount()+1);
node.setTime(System.nanoTime());
sort(); return (V)node.value;
} return null;
}
# 测试
public static void main(String[] args) {
LFU<Integer, String> lruCache = new LFU<>(5);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
lruCache.put(4, "D");
lruCache.put(5, "E");
lruCache.put(6, "F");
lruCache.get(2);
lruCache.get(2);
lruCache.get(3);
lruCache.get(6);
//重新put节点3
lruCache.put(3,"C");
final Map<Integer, Node> caches = (Map<Integer, Node>) lruCache.caches;
for (Map.Entry<Integer, Node> nodeEntry : caches.entrySet()) {
System.out.println(nodeEntry.getValue().value);
}
}