LeetCode LRU 缓存机制(146题)
@author:Jingdai
@date:2020.12.11
题目描述(146题)
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现
LRUCache
类:
LRUCache(int capacity)
以正整数作为容量capacity
初始化 LRU 缓存。int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
思路及代码
这个题目需要各个操作的时间复杂度为 O(1)
,需要仔细思考用什么数据结构。
首先是 key
,如果直接用链表或数组,寻找 key
就需要 O(n)
,显然不行。要想 O(1)
找到一个值,基本就是哈希表了,所以肯定是用哈希表来存贮 key
。
接下来是值,如果直接用类似 HashMap 那样存,我们就无法知道哪一个是最久未使用的数据。所以值的数据肯定是有序的,可以用数组和链表。由于我们需要知道哪一个是最久没有使用的数据,所以每次操作肯定需要移动数据,而数组移动数据的复杂度是 O(n)
,所以就只剩下链表了。对于单链表,删除和移动一个节点需要找到其前驱节点,而单链表得到其前驱节点又要遍历,复杂度就变成了 O(n)
,故理所当然的是使用双链表。
综上,基本的数据结构就是哈希表加双链表。
接下来看实现思路:
-
对于链表的操作,为了不对首尾节点特殊处理,一般会加一个不存贮值的头节点和尾结点,方便代码编写,不需要对首尾节点进行特殊判断。
-
get操作:这里将最近使用的节点放在头节点的后一个,最久没有使用的节点放在尾结点的前一个。当 get 操作没有值时,直接返回 -1;如果有值,先将其对应的节点移动到头结点后一个,再返回其值。
-
put操作:如果
key
已有,将对应节点的值进行更改并将对应的节点移动到头结点后;如果
key
没有值且容量没有到达上限,新建这个节点并添加到头节点后;如果
key
没有值且容量到达上限,新建这个节点并添加到头节点后,然后删除尾结点的前一个节点。
实现过程中会发现一个问题,当需要删除最久未使用的节点时,可以直接删除双链表的尾结点,但是同时还要删除哈希表中的 key
,这时就会出现问题,我们无法知道 key
是什么,所以存贮值的双链表节点不仅需要存贮值,还要存贮它的 key
,这样删除节点时可以删除其在哈希表中的 key
。
最后的代码如下:
class DoubleLinkedNode {
int key;
int value;
DoubleLinkedNode prev;
DoubleLinkedNode next;
DoubleLinkedNode(){}
DoubleLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
private DoubleLinkedNode head;
private DoubleLinkedNode tail;
private int size;
private int capacity;
private HashMap<Integer, DoubleLinkedNode> cache;
public LRUCache(int capacity) {
head = new DoubleLinkedNode();
tail = new DoubleLinkedNode();
cache = new HashMap<>();
head.next = tail;
tail.prev = head;
this.capacity = capacity;
this.size = 0;
}
// head recently used
public int get(int key) {
if (cache.containsKey(key)) {
DoubleLinkedNode node = cache.get(key);
// take out node
node.prev.next = node.next;
node.next.prev = node.prev;
// put it behind the head
node.next = head.next;
node.prev = head;
head.next = node;
node.next.prev = node;
return node.value;
}
return -1;
}
public void put(int key, int value) {
DoubleLinkedNode node = cache.get(key);
if (node != null) {
node.value = value;
// take out node
node.prev.next = node.next;
node.next.prev = node.prev;
// put it behind the head
node.next = head.next;
node.prev = head;
head.next = node;
node.next.prev = node;
} else {
if (this.size < capacity) {
node = new DoubleLinkedNode(key, value);
cache.put(key, node);
// put it behind the head
node.next = head.next;
node.next.prev = node;
head.next = node;
node.prev = head;
size ++;
} else if (this.size == capacity && capacity != 0){
// delete the last
cache.remove(tail.prev.key);
tail.prev.prev.next = tail;
tail.prev = tail.prev.prev;
node = new DoubleLinkedNode(key, value);
cache.put(key, node);
// put it behind the head
node.next = head.next;
node.next.prev = node;
head.next = node;
node.prev = head;
}
}
}