前言
本题是Leetcode的LRU的变种实现
题目链接:LRU 缓存 - leetcode
题目描述:
请你设计并实现一个满足LRU
(最近最少使用) 缓存 约束的数据结构。
实现LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化LRU
缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。- 函数
get
和put
必须以 O ( 1 ) O(1) O(1) 的平均时间复杂度运行。题目归纳:
LRU
缓存在计算机组成原理与OS课程中是常客,在OS中的Cache
替换算法出现次数尤其频繁,先复习下LRU
的定义
LRU
(Least(最少) Recently(近) Used 最近最少使用)侧重于观察最近访问,实现起来比LFU更简单,LFU
(Least Frequently Used 最不经常使用)侧重于根据数据的访问次数所得出的统计规律。
当场景满足以下两个特点时,使用LFU
(1) 长期的数据访问模式是稳定的。
(2) 重视数据项的频率 > 重视数据项的时效,也就是关注长期 > 关注短期。
解题思路:
解法: LRU缓存机制 - leetcode官方题解
一、代码实现
# 提前安装包
$ pip install timeloop
# coding:utf-8
# @Time: 2024/1/25 下午8:35
# @Author: 键盘国治理专家
# @File: LRU_with_timeout.py
# @Description: 设计一个LRU缓存,并且带有超时功能
import datetime
import threading
import time
import schedule
from timeloop import Timeloop
from datetime import timedelta
tl = Timeloop()
class DLinkNode: # 双向链表不要和循环双向链表混淆了
def __init__(self, key, value, add_time):
self.key = key
self.value = value
self.prev = None
self.next = None
self.add_time = add_time # 添加时的时间
# 另一种设计超时的思路是,在初始化每个节点的时候,让每个节点就自带一个超时的类或者任务,到时间了就把自己销毁掉,但这种实现思路不觉得奇怪吗?
class LRUCache:
# 哈希表+双向链表(DLinkNode)
def __init__(self, capacity: int, expire_time: int): # expire_time是过期时间
self.cache = {} # 相当于Java里的HashSet,因为Key值即便重复也没有存储两遍的道理
self.dum_head = DLinkNode(-1, -1, datetime.datetime.now()) # 伪头
self.dum_tail = DLinkNode(-1, -1, datetime.datetime.now()) # 伪尾
self.dum_head.next = self.dum_tail
self.dum_tail.prev = self.dum_head
self.capacity = capacity
self.size = 0
self.expire_time = expire_time
def get(self, key: int): # 那么每次get也要更新expire_time
# 如果在cache中,则直接返回
if key in self.cache:
node = self.cache[key]
node.add_time = datetime.datetime.now()
self.move2Head(node) # 由于是LRU,返回之前必须移动到头部
return node
else:
return -1
def put(self, key: int, value: int): # 那么每次put也要更新expire_time
# 如果在cache中就更新值,再提到头部
if key in self.cache:
node = self.cache[key]
node.add_time = datetime.datetime.now()
node.value = value
self.move2Head(node)
else: # 不在cache中,则需要put添加
node = DLinkNode(key, value, datetime.datetime.now())
self.cache[key] = node
self.add2Head(node)
self.size += 1
# 超过容量限制
if self.size > self.capacity:
# 移除末尾
removed = self.removeTaile()
self.cache.pop(removed.key) # 清除缓存
self.size -= 1
def move2Head(self, node: DLinkNode):
# 拆除旧关系
self.removeNode(node)
# 建立新关系
self.add2Head(node)
def add2Head(self, node: DLinkNode):
# (1)新节点与老节点构建关系
node.prev = self.dum_head
node.next = self.dum_head.next
# (2)拆除旧有关系
self.dum_head.next.prev = node
self.dum_head.next = node
def removeNode(self, node: DLinkNode):
node.prev.next = node.next
node.next.prev = node.prev
return node
# del node # 不能删除,要返回此信息消除cache中的缓存
def removeTail(self):
node = self.dum_tail.prev
self.removeNode(node)
return node
# def __str__(self):
# p = self.dum_head.next
# while p and p != self.dum_tail:
# print(p.value)
# p = p.next
# return ""
def __repr__(self):
p = self.dum_head.next
while p and p != self.dum_tail:
print(p.value, end=" ")
p = p.next
return ""
# 如果要对LRU进行超时的限制,那么必须再单独开一个线程,对LRU里的内容进行扫描,才能有超时,必须用线程来完成这件事
@tl.job(interval=timedelta(seconds=1))
def scanning_LRU(LRU: LRUCache):
print("scanning LRU", id(LRU))
# 反向持续扫描LRU才是对的,因为越靠近tail,越少被用到
# while True: # 应该是一个定时的扫描线程,而不是死循环执行
p = LRU.dum_tail.prev
while p and p != LRU.dum_head:
# 判断是否超时
if (datetime.datetime.now() - p.add_time).seconds > LRU.expire_time: # 超时了,就需要调用删除函数
print(f"\tnode({p.key},{p.value})超时{ (datetime.datetime.now() - p.add_time).seconds - LRU.expire_time}s", )
pre = p.prev
removed = LRU.removeNode(p)
p = removed.prev
# (1)清空缓存
LRU.cache.pop(removed.key)
# (2)物理删除该节点
del removed
else:
p = p.prev
def test_time(a):
start = datetime.datetime.now()
print("----")
time.sleep(2) # 2 seconds
end = datetime.datetime.now()
print((end - start).seconds)
return test_time
if __name__ == '__main__':
capacity = 3
expire_time = 3 # 3s 的超时时间
LRU = LRUCache(capacity, expire_time)
LRU.put(2, 2)
LRU.put(3, 3)
LRU.put(1, 1)
print('LRU超时前的值', LRU)
start = datetime.datetime.now()
cnt = 1e8 * 2
while cnt > 0:
cnt -= 1
print('消磨时间:',datetime.datetime.now() - start) # 大概5-7秒
# 定时调度方式(1),结合timeloop包
scanning_LRU(LRU)
# 定时调度方式(2),schedule方式,该方式实现有问题
# schedule.every(1).seconds.do(lambda: scanning_LRU(LRU))
# 定时调度方式(3),threading.Timer()方式,该方式实现有问题
# thread = threading.Timer(1, lambda: scanning_LRU(LRU))
# thread = threading.Timer(1, scanning_LRU, LRU)
# thread.start()
print('LRU超时后的值', LRU)
总结
(1)思路一,开辟线程去定时循环扫描LRU链表的节点。这是本文的实现思路,定时任务的实现方式见参考文章。
(2)思路二,给每个DLinkNode节点本身带上一个计时器属性,超时就“自爆”销毁自己,但这种实现思路我是没想出来要怎么实现。
参考文章
参考文章或视频链接 |
---|
[1] Pass parameters to schedule |
[2] 《threading.Timer()定时器实现定时任务》 |
[3] 《我整理了8种方案:Python执行定时任务!》 |
[4] 《5种Python使用定时调度任务的方式》 |