导航
前言
昨天在 leetcode 460 上学习LFU算法,看见一个大佬写的O(1) 解法 —— 存储频次的HashMap改为直接用双向链表(最优实现 13ms 双100%),印象颇深,隔了一天之后照着他的思路自己也手写实现了LFU,和原版略有不同,不过思路很值得讲
源码
一、思路
一般来说,我们理解的链表都是这样的:
我们可以通过双链表实现LRU,但是LFU呢?它比LRU要多出一个“访问频次”的属性,只靠双链表似乎并不能满足LFU的设计需求
那么我们用很多条链表来做这件事怎么样?既可以通过链表的特性保证访问时间和顺序的关系,又可以让每个链表记录一个频次,里面存放的都是对应频次的数据:
在每条链表中又按照频次大小顺序连接着,这也可以形容为“链链表”,用代码来讲,就是:LinkedList<LinkedList<Node>>
那么要如何使用这多重链表呢
- 在已有多条链表分别存储数据的情况下,put操作时,在频次最大的链表上进行头插(缓存满时,要删除频次最小的链表的尾节点)
- 在缓存为空时,put操作时,让缓存生成一条频次为0的链表,并在该链表上进行头插
- 在put,get操作命中缓存时,让缓存中被命中的数据节点移动到(当前频次+1)的频次链表上
- 若频次最小的链表仅剩的节点被删除,则删除频次最小的链表(即对“链链表”进行尾删
二、核心定义
1. 类的层次
在我的容器设计中,我的类定义的层次如下
// 最外层,LFU缓存容器类
public class MultiLinkedListLFU<K, V> {
// 中间层,多重链表类
private class MultiLinkedList {
// 最内层,链表节点类
class Entry {
}
}
}
2. 链表节点类
我的链表节点类存放键值对,如下
class Entry {
K key;
V val;
Entry pre, next;
Entry(K key, V val) {
this.key = key;
this.val = val;
}
}
没什么特别的,不熟的同学建议复习下双向链表
3. 多重链表类
1)类的字段
/**
* 记录被访问的频次
*/
int freq;
/**
* 前/后链表
*/
MultiLinkedList pre, next;
/**
* 当前链表的头/尾节点
*/
Entry head, tail;
/**
* 当前链表的长度
*/
private int size;
这个类的实例是一条链表,每条链表都记录着频次的字段freq
,并且有前后指针MultiLinkedList pre, next
指向其自身的上一条/下一条链表,除此之外,头/尾节点和链表长度的字段基本上都是一条双向链表要记录的字段
2)构造方法
/**
* 无参构造方法
*/
MultiLinkedList() {
}
/**
* 有参构造方法
*
* @param freq
*/
MultiLinkedList(int freq) {
this.freq = freq;
}
有参构造方法为实例记录传入的频次,无参则默认生成频次为0的实例
3)方法实现
a. 添加数据方法put
/**
* 添加数据方法
*
* @param key
* @param val
*/
void put(K key, V val) {
// 链表为空/不为空,分情况讨论
if (size == 0) {
head = new Entry(key, val);
tail = head;
++size;
} else {
addToHead(new Entry(key, val));
}
}
addToHead
方法就是头插方法
b. 删除指定key的节点方法
/**
* 通过key删除指定节点
*
* @param key
*/
boolean removeEntryByKey(K key) {
Entry entry = findEntryByKey(key);
if (entry == null) {
//未找到指定节点,删除失败
return false;
} else {
// 当前key得到的节点是尾节点时,直接尾删