14.1 LRU/LFU缓存实现
面试重要程度:⭐⭐⭐⭐⭐
常见提问方式: "设计LRU缓存" "实现LFU算法" "缓存淘汰策略对比"
预计阅读时间:40分钟
🎯 LRU缓存实现
LRU基本概念
LRU (Least Recently Used) 最近最少使用算法,是一种常用的缓存淘汰策略。
核心思想: 当缓存满时,优先淘汰最久未被访问的数据。
LRU算法实现
/**
* LRU缓存实现
* LeetCode 146: LRU Cache
*/
public class LRUCache {
/**
* 双向链表节点
*/
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
/**
* 获取缓存值
* 时间复杂度:O(1)
*/
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果key存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
/**
* 添加缓存值
* 时间复杂度:O(1)
*/
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
} else {
// 如果key存在,先通过哈希表定位,再修改value,并移到头部
node.value = value;
moveToHead(node);
}
}
/**
* 添加节点到头部
*/
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
/**
* 删除节点
*/
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
/**
* 移动节点到头部
*/
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
/**
* 删除尾部节点
*/
private DLinkedNode removeTail() {
DLinkedNode lastNode = tail.prev;
removeNode(lastNode);
return lastNode;
}
}
基于LinkedHashMap的简化实现
/**
* 基于LinkedHashMap的LRU实现
*/
public class LRUCacheSimple extends LinkedHashMap<Integer, Integer> {
private int capacity;
public LRUCacheSimple(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
🔄 LFU缓存实现
LFU基本概念
LFU (Least Frequently Used) 最不经常使用算法,根据数据的历史访问频率来淘汰数据。
核心思想: 当缓存满时,优先淘汰访问频率最低的数据。
LFU算法实现
/**
* LFU缓存实现
* LeetCode 460: LFU Cache
*/
public class LFUCache {
/**
* 缓存节点
*/
class Node {
int key, val, freq;
Node prev, next;
public Node(int key, int val) {
this.key = key;
this.val = val;
this.freq = 1;
}
}
/**
* 双向链表
*/
class DoublyLinkedList {
Node head, tail;
public DoublyLinkedList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public void addFirst(Node node) {
Node next = head.next;
node.next = next;
next.prev = node;
node.prev = head;
head.next = node;
}
public void remove(Node node) {
Node prev = node.prev;
Node next = node.next;
prev.next = next;
next.prev = prev;
}
public Node removeLast() {
if (head.next == tail) {
return null;
}
Node last = tail.prev;
remove(last);
return last;
}
public boolean isEmpty() {
return head.next == tail;
}
}
private int capacity;
private int minFreq;
private Map<Integer, Node> keyToNode;
private Map<Integer, DoublyLinkedList> freqToList;
public LFUCache(int capacity) {
this.capacity = capacity;
this.minFreq = 0;
this.keyToNode = new HashMap<>();
this.freqToList = new HashMap<>();
}
public int get(int key) {
Node node = keyToNode.get(key);
if (node == null) {
return -1;
}
// 增加频率
increaseFreq(node);
return node.val;
}
public void put(int key, int value) {
if (capacity <= 0) return;
Node node = keyToNode.get(key);
if (node != null) {
// 更新已存在的key
node.val = value;
increaseFreq(node);
return;
}
// 添加新key
if (keyToNode.size() >= capacity) {
// 删除最少使用的节点
removeMinFreqNode();
}
// 插入新节点
Node newNode = new Node(key, value);
keyToNode.put(key, newNode);
freqToList.computeIfAbsent(1, k -> new DoublyLinkedList()).addFirst(newNode);
minFreq = 1;
}
private void increaseFreq(Node node) {
int freq = node.freq;
// 从原频率链表中删除
freqToList.get(freq).remove(node);
// 如果原频率链表为空且是最小频率,更新最小频率
if (freqToList.get(freq).isEmpty() && freq == minFreq) {
minFreq++;
}
// 增加频率并添加到新频率链表
node.freq++;
freqToList.computeIfAbsent(node.freq, k -> new DoublyLinkedList()).addFirst(node);
}
private void removeMinFreqNode() {
DoublyLinkedList list = freqToList.get(minFreq);
Node node = list.removeLast();
keyToNode.remove(node.key);
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
腾讯成长空间 6057人发布
查看17道真题和解析