引言
在计算机科学中,缓存是一种重要的技术,用于提高数据访问速度和系统性能。然而,由于缓存空间有限,当缓存满了之后,就需要一种智能的策略来决定哪些数据应该保留,哪些应该被淘汰。LFU(Least Frequently Used,最少使用)算法就是一种常见的缓存淘汰策略,它基于数据项的访问频率来进行优化管理。
LFU算法简介
LFU算法的核心思想是优先淘汰那些访问频率最低的数据项。在缓存达到容量限制时,LFU算法会移除那些被访问次数最少的缓存条目。如果多个条目的访问次数相同,则根据它们最早被访问的时间进行决策,优先删除最早被访问的条目。
LFU算法的工作原理
LFU算法通常需要两种数据结构来实现:
哈希表 :提供O(1)时间复杂度的数据访问和插入。此时哈希表需要两个,一个记录key和当前节点的映射,一个记录频率和节点的映射
双向链表:维护数据项的使用顺序,最近使用的在头部,最久未使用的在尾部。
数据访问和插入的流程如下:
获取数据(Get):从缓存中获取数据,如果数据存在(缓存命中),则更新数据使用的频率,也就是频率+1,同时要删除当前节点之前对应的频率映射;如果数据不存在(缓存未命中),返回 -1。
插入数据(Put):将数据放入缓存,如果数据已经存在,则更新数据值并更新数据使用的频率;如果数据不存在,则将数据插入放入缓存中,并将最低频率设置为1。如果缓存已满,首先需要移除缓存中的元素,然后再插入。
LFU算法的实现
java
import java.util.HashMap;
public class LFUCache {
static class Node{
int key, value, freq = 1;
Node prev, next;
Node(int key, int value){
this.key = key;
this.value = value;
}
}
// 键到节点的映射
private HashMap<Integer, Node> keyToNode = new HashMap<>();
// 频率到虚拟头节点的映射
private HashMap<Integer, Node> freqToDummy = new HashMap<>();
// 最小访问频率
private int minFreq;
// 容量
private int capacity;
public LFUCache(int capacity) {
this.capacity = capacity;
}
private Node newList() {
Node dummy = new Node(0, 0);
dummy.prev = dummy;
dummy.next = dummy;
return dummy;
}
/**
* 根据键获取值。
* 该方法首先尝试从keyToNode映射中获取给定键对应的节点。如果节点不存在,说明该键不存在于数据结构中,方法将返回-1。
* 如果节点存在,则该方法会执行删除操作(del)和频率更改操作(changeFreq),然后再返回节点的值。
* 这种设计可能是为了在获取值的同时,根据获取情况动态调整数据结构,例如实现一种基于频率的缓存淘汰策略。
*
* @param key 需要获取值的键。
* @return 键对应的值,如果键不存在,则返回-1。
*/
public int get(int key) {
// 尝试从映射中获取给定键对应的节点。
Node node = keyToNode.get(key);
// 如果节点不存在,说明键不存在于数据结构中,返回-1。
if(node == null) return -1;
// 修改节点的频率信息,
changeFreq(node);
// 返回节点的值。
return node.value;
}
/**
* 插入一个键值对到缓存中。
* 如果键已经存在,则更新其值,并根据更新后的频率进行调整。
* 如果缓存已满,则移除最低频率的键值对,并插入新的键值对。
*
* @param key 插入或更新的键。
* @param value 插入或更新的值。
*/
public void put(int key, int value) {
// 尝试从映射中获取现有的节点
Node node = keyToNode.get(key);
if(node != null){
// 如果节点存在,更新其值,并准备进行频率更新操作
node.value = value;
changeFreq(node);
return;
}
// 如果缓存已满,需要移除最低频率的节点以腾出空间
if(keyToNode.size() == capacity){
Node cur = freqToDummy.get(minFreq);
Node delNode = cur.prev;
keyToNode.remove(delNode.key);
del(delNode);
if(cur.prev == cur) freqToDummy.remove(minFreq);
}
// 创建新节点,并插入到映射和频率链表中
node = new Node(key, value);
keyToNode.put(key, node);
insert(1, node);
minFreq = 1;
}
/**
* 修改节点的频率。
* 此方法用于更新给定节点的频率,并相应地调整频率列表和最小频率的值。
* 如果更新后的频率导致原有的频率列表头部节点成为一个孤立节点(即它的前向和后向指针都指向自己),
* 则该节点将从频率列表中移除,并且如果该频率原本是最低频率,则最小频率值将增加。
* 最后,使用更新后的频率将节点插入到频率列表的适当位置。
*
* @param node 需要更新频率的节点。
*/
void changeFreq(Node node){
// 1、先删除节点
del(node);
// 根据当前节点的频率获取频率链表的头部节点
Node cur = freqToDummy.get(node.freq);
// 检查当前频率的链表是否为空(即头部节点的前向指针是否指向自己)
if(cur.prev == cur){
// 如果链表为空,则从映射中移除该频率,并检查是否需要调整最小频率值
freqToDummy.remove(node.freq);
if(minFreq == node.freq){
minFreq++;
}
}
// 3、插入到新位置
insert(++node.freq, node);
}
/**
* 将给定节点插入到特定频率链表中。
* 此函数假设频率对应的链表已经存在,或者如果不存在,则会创建一个新的链表。
* 插入操作在链表的头部进行,以保持节点按频率排序。
*
* @param key 节点的键值,用于确定节点应插入到哪个频率链表。
* @param node 要插入的节点。
*/
void insert(int key, Node node){
// 根据频率获取或创建对应的频率链表的头节点。
// 获取频率的头节点
Node cur = freqToDummy.computeIfAbsent(key, k -> newList());
node.prev = cur;
node.next = cur.next;
cur.next.prev = node;
cur.next = node;
}
/**
* 从双向链表中删除给定节点。
* 此函数不返回任何值,因为它操作的是链表的内部结构。
* 它接受一个参数 node,该参数是需要被删除的节点。
*
* @param node 需要被删除的节点。
*/
void del(Node node){
/* 将给定节点的前一个节点的next指针指向给定节点的后一个节点,从而在链表中向前断开给定节点。 */
node.next.prev = node.prev;
/* 将给定节点的后一个节点的prev指针指向给定节点的前一个节点,从而在链表中向后断开给定节点。 */
node.prev.next = node.next;
}
}