问题描述
请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
- 以正整数作为容量 capacity 初始化 LRU 缓存int get(int key)
- 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1。void put(int key, int value)
- 如果关键字 key 已经存在,则变更其数据值 value;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity,则应该逐出最久未使用的关键字。
函数 get
和 put
必须以 O(1) 的平均时间复杂度运行。
示例:
1 | 输入 |
约束:
- 1 <= capacity <= 3000
- 0 <= key <= 10000
- 0 <= value <= 10^5
- 最多调用 2 * 10^5 次 get 和 put
解题思路
LRU (Least Recently Used) 缓存是一种常见的缓存淘汰策略,它基于"最近最少使用"的原则来管理缓存空间。要实现 LRU 缓存,我们需要两个核心数据结构:
- 哈希表:用于 O(1) 时间复杂度查找键值对
- 双向链表:用于维护缓存项的使用顺序
关键思路:
- 双向链表按照使用顺序存储缓存项,链表头部是最近使用的,尾部是最久未使用的
- 哈希表以 key 为键,链表节点为值,用于快速定位链表中的节点
- 每当缓存被访问(get 或 put 操作),将对应节点移动到链表头部
- 当缓存满时,移除链表尾部的节点(最久未使用的项)
下面是 LRU 缓存操作的流程图示意:
1 | 双向链表(使用顺序从左到右:最近使用 → 最久未使用) |
实现细节
我们使用 Golang 实现 LRU 缓存,主要包含以下步骤:
- 定义双向链表节点结构
DLinkedNode
,包含 key、value 和指向前后节点的指针 - 定义 LRU 缓存结构
LRUCache
,包含容量、大小、哈希表和双向链表的头尾节点 - 实现
Constructor
函数初始化 LRU 缓存 - 实现
Get
方法按键获取值并更新使用顺序 - 实现
Put
方法添加或更新键值对,并在必要时淘汰最久未使用的项
关键实现细节:
- 使用哈希表(map)存储 key 到节点的映射,实现 O(1) 查找
- 双向链表用于追踪元素的使用顺序,新节点或被访问的节点移到链表头部
- 创建两个伪节点(head 和 tail)简化边界情况处理
- 每次访问节点(Get 或 Put 操作)时,将节点移到链表头部
- 当缓存满时,移除链表尾部的节点(最久未使用的项)
代码实现
下面是完整的 Golang 实现:
1 | type DLinkedNode struct { |
复杂度分析
-
时间复杂度:
Get
操作:O(1),哈希表查找是 O(1),移动节点到链表头部也是 O(1)Put
操作:O(1),哈希表查找/插入是 O(1),链表操作也是 O(1)
-
空间复杂度:O(capacity),存储 capacity 个键值对需要 O(capacity) 的空间
关键收获
通过实现 LRU 缓存,我们学习了以下重要概念:
- 数据结构组合:哈希表和双向链表的组合可以创建高效的缓存机制
- 缓存淘汰策略:LRU(最近最少使用)是一种常见且有效的缓存淘汰策略
- 双向链表的应用:双向链表适合需要频繁在任意位置插入和删除的场景
- 边界情况处理:使用伪头尾节点(dummy head/tail)可以简化链表操作
LRU 缓存在实际应用中非常广泛,比如:
- 浏览器的缓存管理
- 数据库的缓存层
- 操作系统的页面置换算法
- 分布式缓存系统(如 Redis)中的淘汰策略
这种设计思想和实现技巧在许多系统设计问题中都有应用。