LFU缓存「原理和Python实现方式」

Leetcode第460题概述

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

进阶:
你是否可以在 O(1) 时间复杂度内执行两项操作?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lfu-cache
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

示例

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

LFU原理介绍

LFU(Least Frequently Used)是一种缓存算法,即「最不经常使用算法」,这个缓存算法使用一个计数器来记录条目被访问的概率。通过使用LFU缓存算法,最低访问数的条目会首先被移除。
简单描述实现方式

采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器将增加一。当缓存达到容量并有一个新块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除。

官方题解

在力扣的官方题解中,提到了两种解法:一种是哈希表+平衡二叉树的解法,另一种是采用双哈希表的解法。

哈希表+平衡二叉树

主要思路:
定义缓存结构
cnt 缓存使用的频率
time 缓存使用的时间
key,value 表示键值对

  • 使用哈希表以key为索引存储缓存,建立平衡二叉树S来保存双关键字(cnt,time)
  • 针对get(key)操作,查看一下哈希表中是否有key这个键,有的话需要同时更新哈希表和集合中该缓存的使用频率cnt以及使用时间time,如果没有key这个键则返回-1。
  • 针对put(key,value)操作,首先查看哈希表中是否有对应的键值,如果有的话,操作类似于get(key),但是不同于get(key)更新该缓存的使用频率cnt以及使用时间time,这一步还要更新缓存的value值。如果没有key的话需要重新插入一个缓存,但是在插入缓存之前需要先查看是否达到缓存容量capcity:
  • 如果达到了缓存容量,要删除最近最少使用的缓存,即cnt最小的缓存。平衡二叉树最左边结点,同时也要删除哈希表中对应的索引,最后再向哈希表和平衡二叉树中插入新的索引信息即可。
  • get和put操作时间复杂度O(logn),空间复杂度O(capacity)
    在C++和Java语言中,有内置的平衡二叉树模块,在Python中没有对应的标准库,在Is there a module for balanced binary tree in Python’s standard library?这个问题的描述下,有人给出了BST的另一种方式。
  • 如果仅需要进行搜索,并且列表中的数据已经排好序了,则可以使用Python中的bisect模块提供二分搜索算法。
  • set和dict可以实现为具有O(1)查找的哈希表。 解决Python中大多数问题的方法实际上是“使用字典”。

bisect.bisect_left(a,x,lo = 0,hi = len(a))
找到了插入点X在一个维持有序。参数lo和hi可用于指定应考虑的列表子集;默认情况下,将使用整个列表。如果x在a中已经存在,则插入点将在任何现有条目之前(左侧)。该返回值适合用作list.insert()假定a已经排序的第一个参数。

返回的插入点i将数组a划分为两半,以便左侧和 右侧。
all(val < x for val in a[lo:i])all(val >= x for val in a[i:hi])

因此题解区的Python实现方式也是巧妙使用了上述方法:

# -*- encoding: utf-8 -*-
"""
@File    : prac460.py
@Time    : 2020/4/5 8:56 上午
@Author  : zhengjiani
@Email   : [email protected]
@Software: PyCharm
bisect-数组二等分算法
参考:https://docs.python.org/3.7/library/bisect.html
"""
import bisect


class LFUCache:
    def __init__(self, capacity: int):
        # 容量capacity和计时time
        self.cap, self.time = capacity, 0  
        # 元素形式为:(频率,时间,键)(freq, time, key)
        self.his = []  
        # 使用字典保存双关键字-键值对形式为:key:[val, freq, time]
        self.dic = {}  

    def get(self, key: int) -> int:
    	# key不存在,返回-1
        if key not in self.dic:  
            return -1
        # 更新该缓存的时间
        self.time += 1  
        # 取出值、频率和时间
        val, freq, time = self.dic[key]  
        # 更新该缓存的使用频率
        self.dic[key][1] += 1  # 将频率+1
        # 找到history里的记录并移除原来缓存
        self.his.pop(bisect.bisect_left(self.his, (freq, time, key))) 
        # 将更新后的记录二分插入 
        bisect.insort(self.his, (freq+1, self.time, key))  
        return val

    def put(self, key: int, value: int) -> None:
        if not self.cap:
            return
        self.time += 1
        # 查看哈希表中是否有对应键值
        if key in self.dic:
        	# 取出频率和时间
            _, freq, time = self.dic[key]  
            # 更新值、频率和时间
            self.dic[key][:] = value, freq+1, self.time 
            # 找到history里的记录并移除
            self.his.pop(bisect.bisect_left(self.his, (freq, time, key))) 
            # 将更新后的记录二分插入 
            bisect.insort(self.his, (freq+1, self.time, key))  
        else:  
        	# 无该记录
            self.dic[key] = [value, 1, self.time]
            # history容量已满
            if len(self.his) == self.cap:  
            	# 删除最近最少使用缓存,因为有序,移除history首个元素即对应的键值对
                del self.dic[self.his.pop(0)[2]]  
            # 将新记录插入history
            bisect.insort(self.his, (1, self.time, key))  
            
if __name__ == '__main__':
    capacity = 2
    cache = LFUCache(capacity)
   #["LFUCache","put","put","get","put","get","get","put","get","get","get"]
    print(cache.put(1,1))
    print(cache.put(2,2))
    print(cache.get(1))
    print(cache.put(3,3))
    print(cache.get(2))
    print(cache.get(3))
    print(cache.put(4,4))
    print(cache.get(1))
    print(cache.get(3))
    print(cache.get(4))

双哈希表

主要思路:
定义两个哈希表

  • freq_table :以频率freq为索引,每个索引存一个双向链表,这个链表存放所有使用频率为freq的缓存(key,value,freq)
  • key_table:以键key为索引,每个索引存放缓存对应在链表中的地址。

get 操作后这个缓存的使用频率加一了,所以我们需要更新缓存在哈希表 freq_table 中的位置。已知这个缓存的键 key,值 value,以及使用频率 freq,那么该缓存应该存放到 freq_table 中 freq + 1 索引下的链表中。
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/lfu-cache/solution/lfuhuan-cun-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Python中双向链表尾部插入,头部删除

import collections
class Node:
	def __init__(self, key, val, pre=None, nex=None, freq=0):
		self.pre = pre
		self.nex = nex
		self.freq = freq			#当前节点使用频率
		self.val = val
		self.key = key
	
	#插入节点
	# self-> nex-> self.nex
	def insert(self, nex):
		nex.pre = self
		nex.nex = self.nex
		self.nex.pre = nex
		self.nex = nex

# 创建双向链表,包含值为0的head,tail
def create_linked_list():
	head = Node(0, 0)
	tail = Node(0, 0)
	head.nex = tail
	tail.pre = head
	return (head, tail)

class LFUCache:
	def __init__(self, capacity: int):
		self.capacity = capacity
		self.size = 0			#键值对总数
		self.minFreq = 0		#记录最小的频率,每次容量满了,删这个频率的head.nex
		self.freqMap = collections.defaultdict(create_linked_list)	#key是频率,值是一条双向链表的head, tail,最近操作的节点插入tail前面,则head.nex是最小使用频率的节点,删除时删head.nex
		self.keyMap = {}		#存储键值对,值是node 类型

	#双向链表中删除指定节点
	def delete(self, node):
		if node.pre:			#不是第一个节点,就需要删除,
			node.pre.nex = node.nex	#前后前接起来
			node.nex.pre = node.pre						
			if node.pre is self.freqMap[node.freq][0] and node.nex is self.freqMap[node.freq][-1]: #新的频率中已存在这个节点,且只有这个节点,那就直接把这个新频率删掉,方便后面插入最新数据
				self.freqMap.pop(node.freq)														   
		return node.key												
	
	#增加
	def increase(self, node):
		node.freq += 1			#当前节点频率+1
		self.delete(node)		#旧频率中,删除此节点
		self.freqMap[node.freq][-1].pre.insert(node)	#新频率中,tail节点前插入当前节点
		if node.freq == 1:		#出现频率为1的了,记录一下,下次容量满了先从这里删
			self.minFreq = 1
		elif self.minFreq == node.freq - 1:	#操作最小频率的节点时,从旧频率到新频率时需要检查下旧频率,只有head,tail就不可能从这里删数据了,那就需要把minFreq更新为新频率,下次从这里删
			head, tail = self.freqMap[node.freq - 1]
			if head.nex is tail:		#这个频率里没有实际节点,只有head,tail
				self.minFreq = node.freq#最小频率更新为节点当前频率

	def get(self, key: int) -> int:
		if key in self.keyMap:
			self.increase(self.keyMap[key])
			return self.keyMap[key].val
		return -1

	def put(self, key: int, value: int) -> None:
		if self.capacity != 0:
			if key in self.keyMap:		#有,更新value
				node = self.keyMap[key]
				node.val = value
			else:
				node = Node(key, value)	#没有,新建一个node
				self.keyMap[key] = node
				self.size += 1
			if self.size > self.capacity:	#大于容量
				self.size -= 1										
				deleted = self.delete(self.freqMap[self.minFreq][0].nex)#删除head.nex
				self.keyMap.pop(deleted)
			self.increase(node)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章