Leetcode 146. LRU缓存机制

题目描述

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。

写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在$O(1)$ 时间复杂度内完成这两种操作?

示例:

1
2
3
4
5
6
7
8
9
10
11
cache = LRUCache(2)

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

分析

看到题目要我们实现一个可以存储key-value形式数据的数据结构,并且可以记录最近访问的key值。首先想到的就是用字典来存储key-value结构,这样对于查找操作时间复杂度就是$O(1)$。

但是因为字典本身是无序的,所以我们还需要一个类似于队列的结构来记录访问的先后顺序,这个队列需要支持如下几种操作:

  • 在末尾加入一项
  • 去除最前端一项
  • 将队列中某一项移到末尾

首先考虑列表结构。

对于列表加入有append(),删除有pop()操作,这两个都是$O(1)$的时间复杂度。而对于将队列中某一项移到末尾,因为列表中存储的是哈希表的key,考虑这样一种情况:

1
2
3
4
5
6
7
# 操作
cache = LRUCache(4)
cache.put(3, 2)
cache.put(2, 1)
cache.put(1, 1)
# 操作之后队列:
# queue = [3, 2, 1]

此时我们再进行cache.put(2, 2)的操作,因为2已经存在在哈希表中,这说明队列中已经存在值为2的元素,那么问题来了,如何在常数时间内把它挑出来移到队尾呢?

答案是不行,所以用列表无法实现常数时间复杂度。

之后再考虑单链表。

对于单链表,哈希表的结构类似于{key: ListNode(value)},即键所对应的是一个节点地址,节点的值是value。对于链表,遇到上面那种情况时可以在常数的时间内找到对应的节点,但是如果想将它移到尾部则需要从头遍历到该节点才能保证链表不断,对于这种情况需要的时间复杂度也是$O(n)$

为了解决移到末尾这个问题,需要使用双链表来记录,结构大概如下图所示:

graph LR

subgraph 哈希表
    hash1("hashmap[1]")
    hash2("hashmap[2]")
    hash1 -.- hash2
end

subgraph 双向链表
    head
    node1
    node2
    tail
end
    hash1-->node1
    hash2-->node2
    head-->node1
    node1-->node2
    node2-->tail
    tail-->node2
    node2-->node1
    node1-->head

这个实在是太丑了没眼看,以后不用了

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class ListNode:
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None


class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hashmap = {}
# 新建两个节点 head 和 tail
self.head = ListNode()
self.tail = ListNode()
# 初始化链表为 head <-> tail
self.head.next = self.tail
self.tail.prev = self.head

# 因为get与put操作都可能需要将双向链表中的某个节点移到末尾,所以定义一个方法
def move_node_to_tail(self, key):
# 先将哈希表key指向的节点拎出来,为了简洁起名node
# hashmap[key] hashmap[key]
# | |
# V --> V
# prev <-> node <-> next pre <-> next ... node
node = self.hashmap[key]
node.prev.next = node.next
node.next.prev = node.prev
# 之后将node插入到尾节点前
# hashmap[key] hashmap[key]
# | |
# V --> V
# prev <-> tail ... node prev <-> node <-> tail
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node

def get(self, key: int) -> int:
if key in self.hashmap:
# 如果已经在链表中了久把它移到末尾(变成最新访问的)
self.move_node_to_tail(key)
res = self.hashmap.get(key, -1)
if res == -1:
return res
else:
return res.value

def put(self, key: int, value: int) -> None:
if key in self.hashmap:
# 如果key本身已经在哈希表中了就不需要在链表中加入新的节点
# 但是需要更新字典该值对应节点的value
self.hashmap[key].value = value
# 之后将该节点移到末尾
self.move_node_to_tail(key)
else:
if len(self.hashmap) == self.capacity:
# 去掉哈希表对应项
self.hashmap.pop(self.head.next.key)
# 去掉最久没有被访问过的节点,即头节点之后的节点
self.head.next = self.head.next.next
self.head.next.prev = self.head
# 如果不在的话就插入到尾节点前
new = ListNode(key, value)
self.hashmap[key] = new
new.prev = self.tail.prev
new.next = self.tail
self.tail.prev.next = new
self.tail.prev = new