leetcode-460.LFU缓存

题目解释

设计并实现最不经常使用(LFU)缓存的数据结构。它应该支持以下操作:getput

get(key)- 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value)- 如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,使最不经常使用的项目无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,最近最少使用的键将被去除。

示例

LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 去除 key 2
cache.get(2);       // 返回 -1 (未找到key 2)
cache.get(3);       // 返回 3
cache.put(4, 4);    // 去除 key 1
cache.get(1);       // 返回 -1 (未找到 key 1)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

解题思路

基本数据结构
  • 字典,键为key,值为key对应链表中的节点node,即key:node
  • 双向链表,自定义一个双向链表节点dlNode类,其中
  1. 链表数据为一个列表,包括[key, value, cnt],其中keyvalue分别为缓存中keyvaluecnt用于记录被访问的次数,默认cnt=0
  2. 链表指针包括前指针pre后指针next,即双向链表
  3. 链表中节点顺序是按照访问次数的降序排列,即访问次数越大,越靠前、访问次数相同情况下越“新”越靠前
主要方法逻辑:
  • __init__: 定义容量self.c = capacity,初始化字典self.cache = {},初始化双向链表。这里,在双向链表初始化中,为避免处理头、尾节点时的复杂特判,采用链表通用技巧即增加虚拟节点处理,因为是双向链表,所以加虚拟头尾两个节点并首尾相连
    1. self.head = dlNode(1, 1, float('inf'))# 头节点,定义访问次数正无穷
    2. self.tail = dlNode(-1, -1, float('-inf'))# 尾节点,定义访问次数负无穷
    3. self.head.next = self.tail
    4. self.tail.pre = self.head
  • -refresh: 辅助刷新位置函数,接收一个节点和对应新访问次数作为参数。更新原则是:
    1. 如果节点访问次数小于其前节点的访问次数,说明仍然有序。无需更新
    2. 否则,根据cnt大小尽可能移动到靠前的位置,即移动到所有不大于其访问次数的节点之前,保证越大越“新”越靠前
  • get:get操作意味着其访问次数+1,其实现逻辑:
    如果缓存容量self.c <= 0或目标键值不在缓存字典中,直接返回-1
    否则,通过字典找到该节点,通过节点找到其访问次数,访问次数+1后刷新其位置
    最后,返回其value值
  • put:put操作情况略显复杂,包括以下情况:
    1. 如果缓存容量self.c <= 0,直接返回
    2. 否则,区分待添加值是否已在缓存字典中:
      - 如果已在缓存字典中,则仅简单更新访问次数并刷新位置即可(无需考虑缓存容量),需注意的是这里既要更新访问次数cnt,也要更新可能变化的value值
      - 如果不在缓存字典,则加入之前还要考虑缓存容量是否已满:
      - 如果缓存容量已满,则先剔除链表尾部节点(更 准确的说是self.tail前的那个节点)
      - 加入节点,并前移到尽可能靠前的位置(调用_refresh())

代码实现

class dlNode:
    def __init__(self, key, val, cnt=0):
        self.val = [key, val, cnt]#键、值、访问次数
        self.pre = None
        self.nxt = None


class LFUCache:
    def __init__(self, capacity: int):
        self.cache = {}#通过key保存链表节点,key:node
        self.c = capacity#字典容量
        self.head = dlNode(1, 1, float('inf'))#头节点,定义访问次数正无穷
        self.tail = dlNode(-1, -1, float('-inf'))#尾节点,定义访问次数负无穷
        self.head.nxt = self.tail 
        self.tail.pre = self.head

    def _refresh(self, node, cnt):##辅助函数,对节点node,以访问次数cnt重新定义其位置
        pNode, nNode = node.pre, node.nxt #当前节点的前后节点
        if cnt < pNode.val[2]:#如果访问次数小于前节点的访问次数,无需更新位置
            return
        pNode.nxt, nNode.pre = nNode, pNode#将前后连起来,跳过node位置
        while cnt >= pNode.val[2]:#前移到尽可能靠前的位置后插入
            pNode = pNode.pre
        nNode = pNode.nxt
        pNode.nxt = nNode.pre = node
        node.pre, node.nxt = pNode, nNode

    def get(self, key: int) -> int:
        if self.c <= 0 or key not in self.cache:#如果容量<=0或者key不在字典中,直接返回-1
            return -1
        node = self.cache[key]#通过字典找到节点
        _, value, cnt = node.val#通过节点得到key,value和cnt
        node.val[2] = cnt+1#访问次数+1
        self._refresh(node, cnt+1)#刷新位置
        return value

    def put(self, key: int, value: int) -> None:
        if self.c <= 0:#缓存容量<=0
            return
        if key in self.cache:#已在字典中,则要更新其value,同时访问次数+1刷新位置
            node = self.cache[key]
            _, _, cnt = node.val
            node.val = [key, value, cnt+1]#更新其值
            self._refresh(node, cnt+1)
        else:
            if len(self.cache) >= self.c: #容量已满,先清除掉尾部元素
                tp, tpp = self.tail.pre, self.tail.pre.pre
                self.cache.pop(tp.val[0]) #从字典剔除尾节点
                tpp.nxt, self.tail.pre = self.tail, tpp #首尾相连,跳过原尾节点
            #新建节点,并先插入到队尾
            node = dlNode(key, value)
            node.pre, node.nxt = self.tail.pre, self.tail
            self.tail.pre.nxt, self.tail.pre = node, node
            self.cache[key] = node
            self._refresh(node, 0)
        
# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水花

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值