LRU缓存–链表算法解析
请你设计并实现一个满足 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 <= 105
- 最多调用
2 * 105
次get
和put
这道题目我一开始想到使用HashMap存储键值对,使用双向链表维持LRU关系,但是钻了牛角尖,下意识认为Map结构是<Integer, Integer>,这样的话每次get操作还需要重新遍历双向链表,找到目标节点并前移到链表头部,不满足时间复杂度O(1),其实我们Map结构是<Integer, DlinkNode>更简单,直接一步找到在双向链表中的位置。
LRU 缓存机制可以通过哈希表
辅以双向链表
实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
- 双向链表按照被使用的顺序存储了这些键值对,靠近
头部
的键值对是最近使用
的,而靠近尾部
的键值对是最久未使用
的。 - 哈希表即为普通的哈希映射(HashMap),通过缓存数据的
键
映射到其在双向链表中的位置
。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)
的时间内完成 get
或者 put
操作。具体的方法如下:
- 对于
get
操作,首先判断key
是否存在:- 如果
key
不存在,则返回−1
; - 如果
key
存在,则key
对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
- 如果
- 对于
put
操作,首先判断key
是否存在:- 如果
key
不存在,使用key
和value
创建一个新的节点,在双向链表的头部添加该节点,并将key
和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项; - 如果
key
存在,则与get
操作类似,先通过哈希表定位,再将对应的节点的值更新为value
,并将该节点移到双向链表的头部。
- 如果
上述各项操作中,访问哈希表的时间复杂度为 O(1)
,在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)
。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1)
时间内完成。
在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
1 | public class LRUCache { |