前言
本篇总结下LRU算法
顺便介绍下python的functools.lru_cache装饰器
1、LRU算法
LRU,全称Least Recently Used
简单的说就是:最近最少使用
是个非常有用且好用的数据结构
原理
设计原则
- 如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小
- 也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰
数据结构
- 双向链表
用于记录元素被塞进cache的顺序,用于淘汰最久未被使用的元素
其实仅仅支持记录顺序的话,单向链表也可以
但由于我们是要实现一个cache,对插入和删除的时间复杂度要求都是O(1)
很明显单向链表和数组是不能完成的 - 哈希表
用于直接记录元素的位置
即用O(1)的时间复杂度来拿到链表的元素
操作
- get操作
根据传入的key去哈希表里拿到对应的元素
如果该元素存在,就把元素挪到链表的尾部 - put操作
首先判断key是否在哈希表里
如果在的话就更新值,并把元素挪到链表的尾部
如果不在哈希表里,说明这是一个新的元素
这时候就需要去判断此时cache的容量了
如果超过最大容量,就需要淘汰链表头部的元素,再将新的元素插入链表尾部
如果没超过最大容量,直接在链表尾部追加新的元素
实现
1、用python自带的OrderedDict,即有顺序的字典
自己用时,这个就很方便
但是面试这么写可能人就没了
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.maxsize = capacity
self.lrucache = OrderedDict()
def get(self, key: int) -> int:
if key in self.lrucache:
self.lrucache.move_to_end(key)
return self.lrucache.get(key, -1)
def put(self, key: int, value: int) -> None:
if key in self.lrucache:
del self.lrucache[key]
self.lrucache[key] = value
if len(self.lrucache) > self.maxsize:
self.lrucache.popitem(last=False)
2、从零实现
先实现双向链表
再实现LRU
# 双向链表的节点
class Node:
def __init__(self, value=None, next=None, prev=None):
self.value = value
self.next = next
self.prev = prev
def __str__(self):
return f"<Node: value: {self.value}>"
__repr__ = __str__
# 双向链表,有append和remove功能
class DBL:
def __init__(self):
node = Node("root")
node.prev = node
node.next = node
self.root = node
self.lens = 0
@property
def head(self):
return self.root.next
@property
def tail(self):
return self.root.prev
def append(self, value):
node = Node(value)
self.tail.next = node
node.prev = self.tail
self.root.prev = node
self.lens += 1
def remove(self, node):
if node == self.root:
return False
if node == self.tail:
self.root.prev = node.prev
if node.next:
node.next.prev = node.prev
node.prev.next = node.next
del node
self.lens -= 1
return True
def __str__(self):
return "->".join([str(node.value) for node in self.iter_item()])
__repr__ = __str__
# LRU
class LRU:
def __init__(self, size=10):
self.size = size
self._link = DBL()
self._cache = dict()
def _move_to_recent(self, node):
if node == self._link.tail:
return
# pop node from link
node.prev.next = node.next
node.next.prev = node.prev
# move node to tail
now_tail = self._link.tail
now_tail.next = node
node.prev = now_tail
node.next = None
self._link.root.prev = node
def _append(self, k, v):
self._link.append(v)
self._cache[k] = self._link.tail
# Bind cache key
self._link.tail.cache_key = k
def _expired_not_used(self):
need_expired = self._link.head
self._link.remove(need_expired)
def get(self, k):
node = self._cache.pop(k, None)
if not node:
return
self._move_to_recent(node)
return node.value
def put(self, k, v):
node = self._cache.pop(k, None)
if node:
node.value = v
self._move_to_recent(node)
else:
if self._link.lens == self.size:
self._expired_not_used()
self._append(k, v)
def __str__(self):
return "->".join([f"{node.cache_key}" for node in self._link.iter_item()])
__repr__ = __str__
3、基于java
public class LRUCache {
private Hashtable<Integer, DLinkedNode>
cache = new Hashtable<Integer, DLinkedNode>();
private int count;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.count = 0;
this.capacity = capacity;
head = new DLinkedNode();
head.pre = null;
tail = new DLinkedNode();
tail.post = null;
head.post = tail;
tail.pre = head;
}
public int get(String key) {
DLinkedNode node = cache.get(key);
if(node == null){
return -1; // should raise exception here.
}
// move the accessed node to the head;
this.moveToHead(node);
return node.value;
}
public void set(String key, int value) {
DLinkedNode node = cache.get(key);
if(node == null){
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
this.cache.put(key, newNode);
this.addNode(newNode);
++count;
if(count > capacity){
// pop the tail
DLinkedNode tail = this.popTail();
this.cache.remove(tail.key);
--count;
}
}else{
// update the value.
node.value = value;
this.moveToHead(node);
}
}
/**
* Always add the new node right after head;
*/
private void addNode(DLinkedNode node){
node.pre = head;
node.post = head.post;
head.post.pre = node;
head.post = node;
}
/**
* Remove an existing node from the linked list.
*/
private void removeNode(DLinkedNode node){
DLinkedNode pre = node.pre;
DLinkedNode post = node.post;
pre.post = post;
post.pre = pre;
}
/**
* Move certain node in between to the head.
*/
private void moveToHead(DLinkedNode node){
this.removeNode(node);
this.addNode(node);
}
// pop the current tail.
private DLinkedNode popTail(){
DLinkedNode res = tail.pre;
this.removeNode(res);
return res;
}
}
2、python的functools.lru_cache
在刷题的时候,学到functools.lru_cache这么个记忆化工具
觉得非常美妙,正好它用的也是lru,就放在本篇做个记录
- lru_cache 装饰器在 Python 的 3.2 版本中引入
- 格式:
@functools.lru_cache(maxsize=None, typed=False)
- 参数:参数
maxsize
为最多缓存的次数,如果为None
,则无限制,设置为2n
时,性能最佳;如果typed=True
(注意,在 functools32 中没有此参数),则不同参数类型的调用将分别缓存,例如 f(3) 和 f(3.0) - 效果:缓存最多 maxsize 个此函数的调用结果,从而提高程序执行的效率,特别适合于耗时的函数
源码
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
# 所有 LRU 缓存元素共享的常量:
sentinel = object() # 特殊标记,用来表示缓存未命中
make_key = _make_key # 根据函数参数生成缓存 key
#
# ---------------------------------
# | PREV | DATA(KEY+RESULT) | NEXT|
# ---------------------------------
#
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # 链表各个域
# 存放 key 到 node 的映射
cache = {
}
full = False
cache_get = cache.get
lock = RLock() # 链表更新不是线程安全的,所以需要加锁
root = [] # 关键:环形双向链表
# 根节点两侧分别是访问频率较高和较低的节点
root[:] = [root, root, None, None] # 初始根节点(相当于一个空的头节点)
def wrapper(*args, **kwds):
nonlocal root, full
key = make_key(args, kwds, typed)
with lock:
link = cache_get(key)
if link is not None: # 缓存命中
# 将被访问的节点移动到环形链表的前面(即 root 的前边)
link_prev, link_next, _key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
last = root[PREV]
last[NEXT] = root[PREV] = link
link[PREV] = last
link[NEXT] = root
return result
# 缓存未命中,调用用户函数生成 RESULT
result = user_function(*args, **kwds)
with lock:
if key in cache:
# 考虑到此时锁已经释放,而且 key 已经被缓存了,就意味着上面的
# 节点移动已经做了,缓存也更新了,所以此时什么都不用做。
pass
elif full: # 新增缓存结果,移除访问频率低的节点
# 下面的操作是使用 root 当前指向的节点存储 KEY 和 RESULT
oldroot = root
oldroot[KEY] = key
oldroot[RESULT] = result
# 接下来将原 root 指向的下一个节点作为新的 root,
# 同时将新 root 节点的 KEY 和 RESULT 清空,这样
# 使用频率最低的节点结果就从缓存中移除了。
root = oldroot[NEXT]
oldkey = root[KEY]
oldresult = root[RESULT]
root[KEY] = root[RESULT] = None
del cache[oldkey]
cache[key] = oldroot
else: # 仅仅新增缓存结果
# 新增节点插入到 root 节点的前面
last = root[PREV]
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link
full = (len(cache) >= maxsize)
return result
return wrapper
使用示例
from functools import lru_cache
@lru_cache(None)
def add(x, y):
print("calculating: %s + %s" % (x, y))
return x + y
print(add(1, 2))
print(add(1, 2))
print(add(2, 3))
输出结果:
calculating: 1 + 2
3
3
calculating: 2 + 3
5
第二次调用 add(1, 2) 时,并没有真正执行函数体,而是直接返回缓存的结果
实例
leet上1039题多边形三角剖分的最低得分
给定 N,想象一个凸 N 边多边形,其顶点按顺时针顺序依次标记为 A[0], A[i], ..., A[N-1]
假设您将多边形剖分为 N-2 个三角形
对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 N-2 个三角形的值之和
返回多边形进行三角剖分后可以得到的最低分
from functools import lru_cache
class Solution:
def minScoreTriangulation(self, A: List[int]) -> int:
@lru_cache(None)
def dfs(left, right):
if left + 1 == right: return 0
res = float('inf')
for k in range(left + 1, right):
res = min(res, dfs(left, k) + dfs(k, right) + A[left] * A[k] * A[right])
return res
return dfs(0, len(A) - 1)
结语
对LRU做了个小结