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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章