简介
LRU(Least Recently Used)和LFU(Least Frequently Used)是两种常见的缓存替换算法。
LRU是基于最近使用时间的缓存替换算法。它的基本思想是,当缓存空间不足时,优先淘汰最长时间未被访问的数据。LRU算法维护一个访问顺序链表(或双向链表),每次访问一个数据时,将其移动到链表的头部。当需要淘汰数据时,选择链表尾部的数据进行删除。
LFU则是基于访问频率的缓存替换算法。它的核心思想是,当缓存空间不足时,优先淘汰访问频率最低的数据。LFU算法维护一个频率计数器,用于记录每个数据被访问的次数。每次访问一个数据时,将对应数据的访问次数加1。当需要淘汰数据时,选择访问次数最低的数据进行删除。
LRU和LFU都是为了在有限的缓存空间中提供尽可能高的缓存命中率。LRU更注重保留最近被访问的数据,而LFU更注重保留访问频率较高的数据。
在实际应用中,可以根据具体的场景选择合适的缓存替换算法。LRU算法适用于访问模式具有时间局部性的场景,而LFU算法适用于访问模式具有频率局部性的场景。有些系统还会结合两种算法,根据实际情况动态选择使用哪种算法。
当我们看这个代码的时候,首先需要理解两个缓存算法的概念:LRU(最近最少使用)和LFU(最不经常使用)。
实现逻辑
- LRU缓存算法逻辑:
LRU缓存算法基本思想是保留最近被访问过的数据项,当容量超过预设阈值时,将最久未被访问的数据项从缓存中移除。
在代码中,我们使用LinkedHashMap作为LRU缓存的数据结构。LinkedHashMap是一个有序的HashMap,通过重写removeEldestEntry方法来控制是否移除最老的数据项。具体逻辑如下:
在构造函数中,我们初始化一个LinkedHashMap作为缓存,并重写了removeEldestEntry方法,使其在缓存大小超过capacity时返回true,表示需要移除最老的数据项。
get方法用于从缓存中获取数据。如果数据项存在于缓存中,则将其移动到链表的尾部(表示最近访问过),然后返回数据项的值;否则,返回null。
put方法用于向缓存中插入数据。如果数据项已存在,则更新数据值,并将其移动到链表尾部。如果数据项不存在,我们检查缓存是否已满,如果满了,则移除链表头部的数据项(最久未被访问的数据项),然后插入新数据项到链表尾部。
- LFU缓存算法逻辑:
LFU缓存算法基本思想是保留访问频率最高的数据项,当容量超过预设阈值时,将访问频率最低的数据项从缓存中移除。
在代码中,我们使用一个自定义的LFUCache类来实现LFU缓存。具体逻辑如下:
在构造函数中,我们初始化一个HashMap作为缓存,并设定缓存容量。
get方法用于从缓存中获取数据。如果数据项存在于缓存中,则将其访问频率增加,并通过updateNode方法将其在链表中按照访问频率调整位置,然后返回数据项的值;否则,返回null。
put方法用于向缓存中插入数据。如果数据项已存在,则更新数据值,并将其访问频率增加,并通过updateNode方法将其在链表中按照访问频率调整位置。如果数据项不存在,我们检查缓存是否已满,如果满了,则移除链表头部的数据项(访问频率最低的数据项),然后插入新数据项到链表尾部,并设定访问频率为1。
updateNode 这段代码是用于在LFU缓存算法中更新节点位置的逻辑。它会在以下情况下执行:
- 当要更新的节点的下一个节点不为空,并且当前节点的频率大于或等于下一个节点的频率时,会进入循环
while (curr.next != null && curr.frequency >= curr.next.frequency)
。这意味着当前节点的频率与下一个节点的频率相同或更高。 - 如果在循环内,发现当前节点
curr
不等于要更新的节点node
,则表示需要将节点node
移动到当前节点curr
的位置。
在LFU缓存算法中,当访问频率增加时,我们需要将节点移动到适当的位置,以保持链表中节点按照访问频率从低到高的顺序排列。这段代码就是负责处理这个移动的逻辑。
例如,假设链表中有以下节点(按照频率从低到高的顺序):A -> B -> C -> D。
- 当访问节点B时,B的频率增加,变为了2。此时,循环将
curr
从B移动到C。 - 如果访问节点C,C的频率增加,变为了3。由于C的频率大于D(频率为2),所以循环将
curr
从C移动到D的位置,链表变为A -> B -> D -> C。
在LFUCache类中,updateNode
方法会在get和put操作中调用,用于调整节点的位置。通过这个逻辑,我们可以确保链表中节点按照频率从低到高的顺序排列。
package com.javayh.mv.common.util;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
/**
* <p>
*
* </p>
*
* @author hai ji
* @version 1.0.0
* @since 2023-07-05
*/
class LRUCache<K, V> {
private int capacity;
private LinkedHashMap<K, V> cache;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
return cache.getOrDefault(key, null);
}
public void put(K key, V value) {
cache.put(key, value);
}
}
class LFUCache<K, V> {
private int capacity;
private Map<K, Node<K, V>> cache;
private Node<K, V> head;
public LFUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>(capacity);
}
public V get(K key) {
Node<K, V> node = cache.get(key);
if (node != null) {
node.frequency++;
updateNode(node);
return node.value;
}
return null;
}
public void put(K key, V value) {
if (capacity == 0) {
return;
}
Node<K, V> node = cache.get(key);
if (node != null) {
node.value = value;
node.frequency++;
updateNode(node);
} else {
if (cache.size() >= capacity) {
K evictKey = head.keys.iterator().next();
head.keys.remove(evictKey);
cache.remove(evictKey);
}
Node<K, V> newNode = new Node<>(key, value);
newNode.frequency = 1;
cache.put(key, newNode);
if (head == null || head.frequency > 1) {
Node<K, V> newHead = new Node<>(null, null);
newHead.next = head;
if (head != null) {
head.prev = newHead;
}
head = newHead;
}
head.keys.add(key);
}
}
/*
* 这段代码是用于在LFU缓存算法中更新节点位置的逻辑。它会在以下情况下执行:
*
* 当要更新的节点的下一个节点不为空,并且当前节点的频率大于或等于下一个节点的频率时,会进入循环while (curr.next != null && curr.frequency >= curr.next.frequency)。这意味着当前节点的频率与下一个节点的频率相同或更高。
*
* 如果在循环内,发现当前节点curr不等于要更新的节点node,则表示需要将节点node移动到当前节点curr的位置。
*
* 在LFU缓存算法中,当访问频率增加时,我们需要将节点移动到适当的位置,以保持链表中节点按照访问频率从低到高的顺序排列。这段代码就是负责处理这个移动的逻辑。
*
* 例如,假设链表中有以下节点(按照频率从低到高的顺序):A -> B -> C -> D。
*
* 当访问节点B时,B的频率增加,变为了2。此时,循环将curr从B移动到C。
*
* 如果访问节点C,C的频率增加,变为了3。由于C的频率大于D(频率为2),所以循环将curr从C移动到D的位置,链表变为A -> B -> D -> C。
*
* 在LFUCache类中,updateNode方法会在get和put操作中调用,用于调整节点的位置。通过这个逻辑,我们可以确保链表中节点按照频率从低到高的顺序排列。
*/
private void updateNode(Node<K, V> node) {
Node<K, V> curr = node;
while (curr.next != null && curr.frequency >= curr.next.frequency) {
curr = curr.next;
}
if (curr != node) {
curr.prev.next = node;
node.prev = curr.prev;
if (node.next != null) {
node.next.prev = curr;
}
curr.next = node.next;
node.next = curr;
curr.prev = node;
}
LinkedHashSet<K> keys = node.keys;
if (!keys.isEmpty()) {
K key = keys.iterator().next();
keys.remove(key);
keys.add(key);
}
}
private static class Node<K, V> {
private K key;
private V value;
private int frequency;
//节点的prev指向频率较低的节点,next指向频率较高的节点。当访问某个数据项时,我们通过更新节点的频率和调整节点在链表中的位置来实现LFU缓存算法的逻辑。
private Node<K, V> prev;
private Node<K, V> next;
private LinkedHashSet<K> keys;
public Node(K key, V value) {
this.key = key;
this.value = value;
this.frequency = 0;
this.prev = null;
this.next = null;
this.keys = new LinkedHashSet<>();
}
}
}
public class Test {
public static void main(String[] args) {
// LRU Cache example 长时间为访问
LRUCache<Integer, String> lruCache = new LRUCache<>(3);
lruCache.put(1, "One");
lruCache.put(2, "Two");
lruCache.put(3, "Three");
System.out.println(lruCache.get(2)); // Output: Two
lruCache.put(4, "Four");
System.out.println(lruCache.get(1)); // Output: null (evicted)
System.out.println(lruCache.get(4)); // Output: Four
// LFU Cache example 访问频次最低的
LFUCache<Integer, String> lfuCache = new LFUCache<>(3);
lfuCache.put(1, "One");
lfuCache.put(2, "Two");
lfuCache.put(3, "Three");
System.out.println(lfuCache.get(2)); // Output: Two
System.out.println(lfuCache.get(2)); // Output: Two
lfuCache.put(4, "Four");
System.out.println(lfuCache.get(2)); // Output: Two
System.out.println(lfuCache.get(1)); // Output: null (evicted)
System.out.println(lfuCache.get(4)); // Output: Four
}
}