實現最不經常使用(LFU)緩存算法設計並實現數據結構。
它應該支持以下操作:get 和 put。
get(key) - 如果鍵存在於緩存中,則獲取鍵的值(總是正數),否則返回 -1。
put(key, value) - 如果鍵不存在,請設置或插入值。當緩存達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最近 最少使用的鍵。
「項的使用次數」就是自插入該項以來對其調用 get 和 put 函數的次數之和。使用次數會在對應項被移除後置爲 0 。
進階:
你是否可以在 O(1) 時間複雜度內執行兩項操作?
示例:
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
解法一 :哈希表HashMap+紅黑樹TreeSet
實現一個緩存,當執行put操作時,如果當前元素個數已經到達容量最大值則應該首先刪除最近最不經常使用的元素,然後執行插入操作。
對於緩存的每一項構建一個Node來存儲,其中包含屬性key value count time
count表示該Node節點被使用的次數 time表示該緩存創建以來經過的時間 即最近最不經常使用的元素即爲count最小 time最小的元素,即需要對於一系列Node節點進行count time的二重排序,這裏定義compare方法,代碼如下:
class Node implements Comparable<Node>{
int key, value;
int count;
int time; // 這裏count time要保證元素的唯一性
int freq;
// 注意TreeSet在判斷元素是否重複以及排序順序時均使用該方法
@Override
public int compareTo(Node o) {
return this.count == o.count ?
Integer.compare(this.time, o.time) :
Integer.compare(this.count, o.count);
}
@Override
public String toString() {
return "Node{" + "key=" + key + ", value=" + value + ", count=" + count + ", time=" + time + '}';
}
}
這裏使用TreeSet來實現logN時間內的Node節點的排序相關功能 也可以使用優先級隊列來實現
當執行插入操作時 往哈希表中插入key-Node對應關係 往TreeSet中插入Node節點 由於Node實現compare方法,因此TreeSet會對於所有Node進行排序 此時第一項即爲最近最不經常使用的元素,如果容量已滿 刪除該元素即可。
public class LFUCache {
Map<Integer, Node> map;
TreeSet<Node> tree;
int capacity;
int time; // 用來計算緩存時間
int size;
public LFUCache(int capacity) {
this.map = new HashMap<>();
this.tree = new TreeSet<>();
this.capacity = capacity;
this.time = 0;
this.size = 0;
}
public int get(int key) {
if(capacity == 0) {
return -1;
}
Node n = map.get(key);
if(n == null) {
return -1;
}
tree.remove(n);
n.time = ++time;
n.count = ++n.count;
tree.add(n);
System.out.println("get first " + tree.first().key);
for(Node t: tree){
System.out.println(t.toString());
}
return n.value;
}
public void put(int key, int value) {
if(capacity == 0) {
return;
}
Node n = map.get(key);
// 如果key已經存在 則更新value
if(n != null){
tree.remove(n);
n.time = ++time;
n.count = ++n.count;
n.value = value;
tree.add(n);
System.out.println("put update first " + tree.first().key);
return;
}
// 如果key不存在 容量已經溢出 則刪除最近最不訪問的元素 即紅黑樹的最左節點
if(size >= capacity){
Node theLeftMost = tree.first();
tree.remove(theLeftMost);
map.remove(theLeftMost.key);
size--;
theLeftMost = null;
}
// 插入新節點
Node newNode = new Node();
newNode.key = key;
newNode.value = value;
newNode.count = 0;
newNode.time = ++time;
map.put(key, newNode);
tree.add(newNode);
size++;
System.out.println("put add first " + tree.first().key + " size= " + size);
for(Node t: tree){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
LFUCache obj = new LFUCache(2);
obj.put(1, 1);
obj.put(2, 2);
obj.get(1);
obj.put(3, 3);
System.out.println(obj.get(2));
}
}
其中get put操作時間複雜度均爲logN
解法二:哈希表HashMap+雙向鏈表LinkedHashSet
Node節點包含 key value freq屬性
定義一個minFreq來保存當前全局最小的使用頻率 當刪除元素時該值要相應更新
使用哈希表來存儲key-Node
另一個哈希表來存儲freq-List 其中freq爲使用頻率 List爲所有相同使用頻率的Node列表 這裏使用LinkedHashSet對於相同頻率的元素最近最不經常使用的Node位於鏈表首元素 實際上也是對於一系列Node元素的使用頻率和訪問時間的二重排序。
對於get 根據key-Node來查詢相應的Node,得到freq,進而根據freq-List得到freq列表 由於Node被訪問一次 因此應該將Node從freq列表中刪除放入到freq+1列表中
對於put 查詢key-Node 如果Node存在 則執行相應更新操作即可 如果Node不存在 則判斷容量是否溢出 如果已滿則首先將最小頻率minFeq對應的List首元素刪除,然後新建Node節點來執行插入操作。
public class LFUCache2 {
// key 使用頻率fre LinkedHashSet<Node> 具有相同使用頻率fre的雙向Node列表
Map<Integer, LinkedHashSet<Node>> freqMap;
// key 緩存鍵值對key value 該key對應的Node
Map<Integer, Node> keyMap;
// 最小使用頻率
// 最小頻率更新時機
// 當有刪除操作時 如果當前freq==minFreq並且刪除元素之後鏈表爲空則應該更新minFreq
int minFreq;
int size;
int capacity;
public LFUCache2(int capacity) {
this.freqMap = new HashMap<>();
this.keyMap = new HashMap<>();
this.minFreq = 0;
this.size = 0;
this.capacity = capacity;
}
public int get(int key) {
if(capacity == 0) return -1;
Node n = keyMap.get(key);
if(null == n) return -1;
int freq = n.freq;
// 從freq鏈表中刪除
freqMap.get(freq).remove(n);
if(freq == minFreq && freqMap.get(freq).size() == 0){
minFreq = freq + 1;
}
n.freq = ++n.freq;
if(freqMap.get(freq+1) == null){
freqMap.put(freq+1, new LinkedHashSet<>());
}
// 添加到freq+1鏈表中
freqMap.get(freq+1).add(n);
return n.value;
}
public void put(int key, int value) {
if(capacity == 0) return;
Node n = keyMap.get(key);
// 更新操作
if(null != n){
n.value = value;
freqMap.get(n.freq).remove(n);
if(n.freq == minFreq && freqMap.get(n.freq).size() == 0){
minFreq = n.freq + 1;
}
n.freq = ++n.freq;
if(freqMap.get(n.freq) == null){
freqMap.put(n.freq, new LinkedHashSet<>());
}
// 添加到freq+1鏈表中
freqMap.get(n.freq).add(n);
return;
}
// 超出容量 則刪除使用頻率最小的Node
if(size>=capacity){
// 對應最小使用頻率下標鏈表的首元素即爲應該刪除元素
LinkedHashSet<Node> theMinOldList = freqMap.get(minFreq);
Node minOld = theMinOldList.iterator().next();
freqMap.get(minFreq).remove(minOld);
keyMap.remove(minOld.key);
size--;
// 如果最小值索引鏈表爲空 則更新最小值索引
if(theMinOldList.size() == 0){
minFreq = minFreq + 1;
}
}
Node newNode = new Node();
newNode.freq = 0;
newNode.key = key;
newNode.value = value;
keyMap.put(key, newNode);
if(freqMap.get(newNode.freq) == null){
freqMap.put(newNode.freq, new LinkedHashSet<>());
}
freqMap.get(newNode.freq).add(newNode);
size++;
minFreq = 0;
}
public static void main(String[] args) {
LFUCache2 obj = new LFUCache2(2);
obj.put(1, 1);
obj.put(2, 2);
obj.get(1);
obj.put(3, 3);
System.out.println(obj.get(2));
}
}
時間複雜度爲O(1)
基本思想 利用哈希表來保存key-Node對應關係 利用其它數據結構來保存Node 並且實現Node高效的排序 增加 刪除 查詢功能。核心在於HashMap TreeSet LinkedHashSet數據結構。
LFU與LRU
LRU (Least Recently Used)緩存機制(看時間)
在緩存滿的時候,刪除緩存裏最久未使用的數據,然後再放入新元素;
數據的訪問時間很重要,訪問時間距離現在越近,就越不容易被刪除;
就是喜新厭舊,淘汰在緩存裏呆的時間最久的元素。在刪除元素的時候,只看「時間」這一個維度。
LFU (Least Frequently Used)緩存機制(看訪問次數)
在緩存滿的時候,刪除緩存裏使用次數最少的元素,然後在緩存中放入新元素;
數據的訪問次數很重要,訪問次數越多,就越不容易被刪除;
根據題意,「當存在平局(即兩個或更多個鍵具有相同使用頻率)時,最近最少使用的鍵將被去除」,即在「訪問次數」相同的情況下,按照時間順序,先刪除在緩存裏時間最久的數據。
核心思想:先考慮訪問次數,在訪問次數相同的情況下,再考慮緩存的時間。
順帶搞一下LRUCache 實際上就是對於Node節點的排序規則僅考慮時間這一因素 當然該實現方法不是最優的。
可以參考HashMap+LinkedList
基於HashMap+TreeSet來實現LRUCache
public class LRUCache {
Map<Integer, Node> map;
TreeSet<Node> tree;
int capacity;
int time; // 用來計算緩存時間
int size;
public LRUCache(int capacity) {
this.map = new HashMap<>();
this.tree = new TreeSet<>(Comparator.comparingInt(o -> o.time));
this.capacity = capacity;
this.time = 0;
this.size = 0;
}
public int get(int key) {
if(capacity == 0) {
return -1;
}
Node n = map.get(key);
if(n == null) {
return -1;
}
tree.remove(n);
n.time = ++time;
tree.add(n);
return n.value;
}
public void put(int key, int value) {
if(capacity == 0) {
return;
}
Node n = map.get(key);
// 如果key已經存在 則更新value
if(n != null){
tree.remove(n);
n.time = ++time;
n.value = value;
tree.add(n);
return;
}
// 如果key不存在 容量已經溢出 則刪除最近最不訪問的元素 即紅黑樹的最左節點
if(size >= capacity){
Node theLeftMost = tree.first();
tree.remove(theLeftMost);
map.remove(theLeftMost.key);
size--;
theLeftMost = null;
}
// 插入新節點
Node newNode = new Node();
newNode.key = key;
newNode.value = value;
newNode.time = ++time;
map.put(key, newNode);
tree.add(newNode);
size++;
}
}