LRU Cache 与 LFU Cache

浅谈一下缓存中的LRU与LFU策略,针对get(key)put(key,value)两个接口实现任何操作时间复杂度O(1)的数据结构。

LRU

我们维护一个队列,这个队列永远把最近插入/使用的放在顶端,最小最近使用的放在底端。对于put方法要么更新已有的值,要么新加入一个值,无论更新还是新插入都要放在顶端,如果当前队列已满并且还需要插入新值,则删除底端的值后再插入新的值。对于get方法,要么没找到返回-1,要么返回查询结果,并把这个值放在顶端。

上述策略可以满足LRU,但是如何保证操作都是O(1)呢?我们比较耗时的操作是在挪动value的位置,比如更新一个value后,要将其放在队列的顶端。如果是一般的Array存储,那么时间复杂度是O(n),所以我们选择双向链表。但是这样一来查找的复杂度就变成O(n)了,我们只需要用hashmap辅助,hashmap的value记录每个链表结点的引用即可。

这样删除的时候,直接删除尾指针指向的结点,并删除hashmap中的pair。在挪动结点的时候是不必更新hashmap的,因为hashmap存储的是引用而非绝对位置。

图示为key是3, 4, 8的情况

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
# Leetcode 146
class Node:
def __init__(self, key: int, value: int):
self.key = key
self.value = value
self.pre = None
self.next = None


class LRUCache:

def __init__(self, capacity: int):
self.cap = capacity
self.linkedMap = {}
self.head = Node(-1, -1)
self.tail = Node(-1, -1)
self.head.next = self.tail
self.tail.pre = self.head

def get(self, key: int) -> int:
if key in self.linkedMap:
node = self.linkedMap[key]
self._remove(node)
self._add(node)
return node.value
return -1

def put(self, key: int, value: int) -> None:
if key in self.linkedMap:
self._remove(self.linkedMap[key])
elif len(self.linkedMap) + 1 > self.cap:
self._remove(self.tail.pre)
self._add(Node(key, value))

def _remove(self, node: Node):
node.pre.next = node.next
node.next.pre = node.pre
self.linkedMap.pop(node.key)

def _add(self, node: Node):
node.next = self.head.next
node.pre = self.head
self.head.next.pre = node
self.head.next = node
self.linkedMap[node.key] = node

LFU

与LRU一样要用双向链表存储结点,针对每一个频率都用一个单独的链表存储,不同的频率链表之间按顺序首尾相连,逻辑上可以看作链表的链表。新加入的value都要放入频率为1的链表的表头,这是初始情况。当更新一个结点时,比如频率3的被get了一次,那么要把他从频率3的链表拿出来放入频率4的链表的表头,这样put和get的更新操作都是O(1)的复杂度。

删除的时候,我们都从频率最小的链表的末尾开始删除,也就是tail指针始终指向当前频率最小的链表的末尾,tail指针的维护时间复杂度也是O(1)的。(为了实现方便,我这部分不是严格O(1)的)

同样用hashmap保存key到node的映射,这一点和LRU是一致的。

图示为存在1,2,3三种频率的情况

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# Leetcode 460
class Node:
def __init__(self, k, v):
self.key = k
self.value = v
self.freq = 0
self.next = None
self.prev = None


class LFUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.freqHead = {}
self.freqTail = {}
self.keyMap = {}

def get(self, key: int) -> int:
ans = -1
if key in self.keyMap:
node = self.keyMap[key]
ans = node.value
self._update(node)
return ans

def _print(self, tail):
x = []
while tail:
x.append(tail.value)
tail = tail.prev
print(x)

def put(self, key: int, value: int) -> None:
if self.cap == 0:
return

if key in self.keyMap:
self.keyMap[key].value = value
self._update(self.keyMap[key])
return
elif len(self.keyMap) == self.cap:
self._removeTail()

node = Node(key, value)
node.freq = 1
self._addFreq(node.freq, node)

def _update(self, node):
key, value, freq = node.key, node.value, node.freq
self._remove(node)
self._addFreq(freq + 1, Node(key, value))

def _removeTail(self): # 这部分我偷懒了,可以优化到严格的O(1)操作
tail = self.freqTail[1]
while tail and tail.key == -1:
tail = tail.prev
self._remove(tail)

def _addFreq(self, freq, node):
head = None
if freq in self.freqHead:
head = self.freqHead[freq]
else:
head = Node(-1, -1)
tail = Node(-1, -1)
head.next = tail
tail.prev = head
if freq > 1:
tail.next = self.freqHead[freq - 1]
tail.next.prev = tail
self.freqHead[freq] = head
self.freqTail[freq] = tail
self._add(node, head)
node.freq = freq

def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
self.keyMap.pop(node.key)

def _add(self, node, head):
node.next = head.next
node.prev = head
head.next.prev = node
head.next = node
self.keyMap[node.key] = node

总结

某单一类型的数据结构不足以支撑当前的需求时,可以使用多种数据结构组合完成任务。