1492 字
7 分钟
LFU 缓存

题目描述#

请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。

实现 LFUCache 类:

  • LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
  • int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
  • void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

解题思路#

  1. 双哈希表结构
    • key_map:存储键到节点(值+频率)的映射
    • freq_map:存储频率到双向链表的映射
  2. 双向链表
    • 每个频率对应一个链表,维护节点访问时序
    • 链表头为最近访问,链表尾为最久未访问
  3. 最小频率追踪
    • 动态维护当前最小使用频率 min_freq
  4. 操作处理
    • get:更新节点频率,调整链表位置
    • put:插入新节点或更新现有节点
  5. 淘汰机制
    • 缓存满时删除 min_freq 链表尾节点

关键洞察#

  1. 频率分层
    • 相同频率节点通过双向链表维护时序
  2. O(1)操作
    • 哈希表实现常数时间节点访问
    • 链表实现常数时间节点增删
  3. 最小频率更新
    • 插入新节点时 min_freq=1
    • 删除节点后检查链表空则 min_freq++
  4. 原子操作
    • 节点频率更新需先移除后重新插入
  5. 高效淘汰
    • 直接访问 min_freq 链表尾节点删除

复杂度分析#

指标说明
时间复杂度O(1):get/put 操作均为常数时间
空间复杂度O(n):存储所有节点和链表结构
最优情况O(1):缓存未满时操作
最坏情况O(1):缓存满时淘汰操作

代码实现#

class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.freq = 1;
this.prev = null;
this.next = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = new Node(null, null);
this.tail = new Node(null, null);
this.head.next = this.tail;
this.tail.prev = this.head;
this.size = 0;
}
addToHead(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
this.size++;
}
remove(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
this.size--;
}
removeTail() {
if (this.size === 0) return null;
const node = this.tail.prev;
this.remove(node);
return node;
}
}
class LFUCache {
constructor(capacity) {
this.capacity = capacity;
this.minFreq = 0;
this.size = 0;
this.keyMap = new Map();
this.freqMap = new Map();
}
get(key) {
if (!this.keyMap.has(key)) return -1;
const node = this.keyMap.get(key);
this.updateNode(node);
return node.value;
}
put(key, value) {
if (this.capacity === 0) return;
if (this.keyMap.has(key)) {
const node = this.keyMap.get(key);
node.value = value;
this.updateNode(node);
return;
}
if (this.size >= this.capacity) {
const minList = this.freqMap.get(this.minFreq);
const deleted = minList.removeTail();
this.keyMap.delete(deleted.key);
this.size--;
}
const newNode = new Node(key, value);
this.keyMap.set(key, newNode);
if (!this.freqMap.has(1)) {
this.freqMap.set(1, new DoublyLinkedList());
}
this.freqMap.get(1).addToHead(newNode);
this.minFreq = 1;
this.size++;
}
updateNode(node) {
const oldFreq = node.freq;
const oldList = this.freqMap.get(oldFreq);
oldList.remove(node);
if (oldList.size === 0 && oldFreq === this.minFreq) {
this.minFreq++;
}
node.freq++;
const newFreq = node.freq;
if (!this.freqMap.has(newFreq)) {
this.freqMap.set(newFreq, new DoublyLinkedList());
}
this.freqMap.get(newFreq).addToHead(node);
}
}
use std::collections::HashMap;
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
key: i32,
value: i32,
freq: i32,
prev: Option<Rc<RefCell<Node>>>,
next: Option<Rc<RefCell<Node>>>,
}
impl Node {
fn new(key: i32, value: i32) -> Self {
Node {
key,
value,
freq: 1,
prev: None,
next: None,
}
}
}
struct DoublyLinkedList {
head: Rc<RefCell<Node>>,
tail: Rc<RefCell<Node>>,
size: usize,
}
impl DoublyLinkedList {
fn new() -> Self {
let head = Rc::new(RefCell::new(Node::new(0, 0)));
let tail = Rc::new(RefCell::new(Node::new(0, 0)));
head.borrow_mut().next = Some(tail.clone());
tail.borrow_mut().prev = Some(head.clone());
DoublyLinkedList { head, tail, size: 0 }
}
fn add_to_head(&mut self, node: Rc<RefCell<Node>>) {
let next = self.head.borrow().next.clone();
node.borrow_mut().prev = Some(self.head.clone());
node.borrow_mut().next = next.clone();
self.head.borrow_mut().next = Some(node.clone());
next.unwrap().borrow_mut().prev = Some(node.clone());
self.size += 1;
}
fn remove(&mut self, node: Rc<RefCell<Node>>) {
let prev = node.borrow().prev.clone().unwrap();
let next = node.borrow().next.clone().unwrap();
prev.borrow_mut().next = Some(next.clone());
next.borrow_mut().prev = Some(prev);
self.size -= 1;
}
fn remove_tail(&mut self) -> Option<Rc<RefCell<Node>>> {
if self.size == 0 {
return None;
}
let node = self.tail.borrow().prev.clone().unwrap();
self.remove(node.clone());
Some(node)
}
}
struct LFUCache {
capacity: usize,
min_freq: i32,
size: usize,
key_map: HashMap<i32, Rc<RefCell<Node>>>,
freq_map: HashMap<i32, DoublyLinkedList>,
}
impl LFUCache {
fn new(capacity: i32) -> Self {
LFUCache {
capacity: capacity as usize,
min_freq: 0,
size: 0,
key_map: HashMap::new(),
freq_map: HashMap::new(),
}
}
fn get(&mut self, key: i32) -> i32 {
match self.key_map.get(&key) {
Some(node) => {
let node = node.clone();
self.update_node(node.clone());
node.borrow().value
},
None => -1,
}
}
fn put(&mut self, key: i32, value: i32) {
if self.capacity == 0 {
return;
}
if let Some(node) = self.key_map.get(&key) {
let node = node.clone();
node.borrow_mut().value = value;
self.update_node(node);
return;
}
if self.size >= self.capacity {
if let Some(list) = self.freq_map.get_mut(&self.min_freq) {
if let Some(tail) = list.remove_tail() {
self.key_map.remove(&tail.borrow().key);
self.size -= 1;
}
}
}
let new_node = Rc::new(RefCell::new(Node::new(key, value)));
self.key_map.insert(key, new_node.clone());
self.freq_map.entry(1)
.or_insert_with(DoublyLinkedList::new)
.add_to_head(new_node);
self.min_freq = 1;
self.size += 1;
}
fn update_node(&mut self, node: Rc<RefCell<Node>>) {
let old_freq = node.borrow().freq;
let new_freq = old_freq + 1;
if let Some(list) = self.freq_map.get_mut(&old_freq) {
list.remove(node.clone());
if list.size == 0 && old_freq == self.min_freq {
self.min_freq += 1;
}
}
node.borrow_mut().freq = new_freq;
self.freq_map.entry(new_freq)
.or_insert_with(DoublyLinkedList::new)
.add_to_head(node);
}
}
class Node:
__slots__ = ('key', 'value', 'freq', 'prev', 'next')
def __init__(self, key, value):
self.key = key
self.value = value
self.freq = 1
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def add_to_head(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
self.size += 1
def remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
def remove_tail(self):
if self.size == 0:
return None
node = self.tail.prev
self.remove(node)
return node
class LFUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.min_freq = 0
self.size = 0
self.key_map = {}
self.freq_map = {}
def get(self, key: int) -> int:
if key not in self.key_map:
return -1
node = self.key_map[key]
self._update_node(node)
return node.value
def put(self, key: int, value: int) -> None:
if self.capacity == 0:
return
if key in self.key_map:
node = self.key_map[key]
node.value = value
self._update_node(node)
return
if self.size >= self.capacity:
min_list = self.freq_map.get(self.min_freq)
if min_list and min_list.size > 0:
deleted = min_list.remove_tail()
if deleted:
self.key_map.pop(deleted.key)
self.size -= 1
new_node = Node(key, value)
self.key_map[key] = new_node
if 1 not in self.freq_map:
self.freq_map[1] = DoublyLinkedList()
self.freq_map[1].add_to_head(new_node)
self.min_freq = 1
self.size += 1
def _update_node(self, node):
old_freq = node.freq
new_freq = old_freq + 1
if old_freq in self.freq_map:
old_list = self.freq_map[old_freq]
old_list.remove(node)
if old_list.size == 0 and old_freq == self.min_freq:
self.min_freq += 1
node.freq = new_freq
if new_freq not in self.freq_map:
self.freq_map[new_freq] = DoublyLinkedList()
self.freq_map[new_freq].add_to_head(node)

实际应用场景#

  1. 数据库缓存:MySQL查询结果缓存`
  2. CDN系统: 内容分发网络的热点资源管理`
  3. 操作系统:内存页面置换算法`
  4. 推荐系统:用户行为频率跟踪`

相似题目#

LFU 缓存
https://website-truelovings-projects.vercel.app/posts/algorithms/lfu-缓存/
作者
欢迎来到StarSky的网站!
发布于
2025-09-01
许可协议
CC BY-NC-SA 4.0