数据结构系列:LRU算法与python的functools.lru_cache装饰器

前言

本篇总结下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做了个小结

猜你喜欢

转载自blog.csdn.net/weixin_44604541/article/details/108940187
今日推荐