|
| 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