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)