Skip to content

Commit caf4cde

Browse files
committed
增加 LRU 实现
1 parent 70e55c9 commit caf4cde

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

docs/03_链表/lru_cache.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
python3 only
3+
LRU cache
4+
"""
5+
from collections import OrderedDict
6+
from functools import wraps
7+
8+
9+
def fib(n):
10+
if n <= 1:
11+
return 1
12+
return f(n - 1) + f(n - 2) # 由于涉及到重复计算,这个递归函数在 n 大了以后会非常慢
13+
14+
15+
def cache(func):
16+
"""先引入一个简单的装饰器缓存,其实原理很简单,就是内部用一个字典缓存已经计算过的结果"""
17+
store = {}
18+
19+
@wraps(func)
20+
def _(n):
21+
if n in store:
22+
return store[n]
23+
else:
24+
res = func(n)
25+
store[n] = res
26+
return res
27+
return _
28+
29+
30+
@cache
31+
def f(n):
32+
if n <= 1:
33+
return 1
34+
return f(n - 1) + f(n - 2)
35+
36+
37+
"""
38+
问题来了,假如空间有限怎么办,我们不可能一直向缓存塞东西,当缓存达到一定个数之后,我们需要一种策略踢出一些元素,
39+
用来给新的元素腾出空间。
40+
一般缓存失效策略有
41+
- LRU(Least-Recently-Used): 替换掉最近请求最少的对象,实际中使用最广。cpu缓存淘汰和虚拟内存效果好,web应用欠佳
42+
- LFU(Least-Frequently-Used): 缓存污染问题(一个先前流行的缓存对象会在缓存中驻留很长时间)
43+
- First in First out(FIFO)
44+
- Random Cache: 随机选一个删除
45+
46+
LRU 是常用的一个,比如 redis 就实现了这个策略,这里我们来模拟实现一个。
47+
要想实现一个 LRU,我们需要一种方式能够记录访问的顺序,并且每次访问之后我们要把最新使用到的元素放到最后(表示最新访问)。
48+
当容量满了以后,我们踢出最早访问的元素。假如用一个链表来表示的话:
49+
50+
[1] -> [2] -> [3]
51+
52+
假设最后边是最后访问的,当访问到一个元素以后,我们把它放到最后。当容量满了,我们踢出第一个元素就行了。
53+
一开始的想法可能是用一个链表来记录访问顺序,但是单链表有个问题就是如果访问了中间一个元素,我们需要拿掉它并且放到链表尾部。
54+
而单链表无法在O(1)的时间内删除一个节点(必须要先搜索到它),但是双端链表可以,因为一个节点记录了它的前后节点,
55+
只需要把要删除的节点的前后节点链接起来就行了。
56+
还有个问题是如何把删除后的节点放到链表尾部,如果是循环双端链表就可以啦,我们有个 root 节点链接了首位节点,
57+
只需要让 root 的前一个指向这个被删除节点,然后让之前的最后一个节点也指向它就行了。
58+
59+
使用了循环双端链表之后,我们的操作就都是 O(1) 的了。这也就是使用一个 dict 和一个 循环双端链表 实现LRU 的思路。
60+
不过一般我们使用内置的 OrderedDict(原理和这个类似)就好了,要实现一个循环双端链表是一个不简单的事情。
61+
62+
"""
63+
64+
65+
class LRUCache:
66+
def __init__(self, capacity=128):
67+
self.capacity = capacity
68+
# 借助 OrderedDict 我们可以快速实现一个 LRUCache,OrderedDict 内部其实也是使用循环双端链表实现的
69+
# OrderedDict 有两个重要的函数用来实现 LRU,一个是 move_to_end,一个是 popitem,请自己看文档
70+
self.od = OrderedDict()
71+
72+
def get(self, key):
73+
self.od.move_to_end(key) # 每次访问就把key 放到最后表示最新访问
74+
return self.od[key]
75+
76+
def add_or_update(self, key, value):
77+
if key in self.od: # update
78+
self.od[key] = value
79+
self.od.move_to_end(key)
80+
else: # insert
81+
self.od[key] = value
82+
if len(self.od) > self.capacity:
83+
print("FULL ||||||||||||||||")
84+
self.od.popitem(last=False)
85+
86+
def __call__(self, func):
87+
def _(n):
88+
if n in self.od:
89+
return self.get(n)
90+
else:
91+
val = func(n)
92+
self.add_or_update(n, val)
93+
return val
94+
return _
95+
96+
97+
@LRUCache(10)
98+
def f_use_lru(n):
99+
if n <= 1:
100+
return 1
101+
return f(n - 1) + f(n - 2)
102+
103+
104+
def test():
105+
import time
106+
beg = time.time()
107+
for i in range(34):
108+
print(f(i))
109+
print(time.time() - beg)
110+
beg = time.time()
111+
for i in range(34):
112+
print(f_use_lru(i))
113+
print(time.time() - beg)
114+
115+
116+
if __name__ == '__main__':
117+
test()

0 commit comments

Comments
 (0)