Hard | LeetCode 460. LFU 快取 | 設計(HashMap+雙向連結串列)(HashMap+TreeSet)
LeetCode 460. LFU 快取
請你為 最不經常使用(LFU)快取演算法設計並實現資料結構。
實現 LFUCache
類:
LFUCache(int capacity)
- 用資料結構的容量capacity
初始化物件int get(int key)
- 如果鍵存在於快取中,則獲取鍵的值,否則返回 -1。void put(int key, int value)
- 如果鍵已存在,則變更其值;如果鍵不存在,請插入鍵值對。當快取達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最近最久未使用 的鍵。
注意「項的使用次數」就是自插入該項以來對其呼叫 get
和 put
函式的次數之和。使用次數會在對應項被移除後置為 0 。
為了確定最不常使用的鍵,可以為快取中的每個鍵維護一個 使用計數器 。使用計數最小的鍵是最久未使用的鍵。
當一個鍵首次插入到快取中時,它的使用計數器被設定為 1
(由於 put 操作)。對快取中的鍵執行 get
或 put
操作,使用計數器的值將會遞增。
示例:
輸入: ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]] 輸出: [null, null, null, 1, null, -1, 3, null, -1, 3, 4] 解釋: // cnt(x) = 鍵 x 的使用計數 // cache=[] 將顯示最後一次使用的順序(最左邊的元素是最近的) LFUCache lFUCache = new LFUCache(2); lFUCache.put(1, 1); // cache=[1,_], cnt(1)=1 lFUCache.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1 lFUCache.get(1); // 返回 1 // cache=[1,2], cnt(2)=1, cnt(1)=2 lFUCache.put(3, 3); // 去除鍵 2 ,因為 cnt(2)=1 ,使用計數最小 // cache=[3,1], cnt(3)=1, cnt(1)=2 lFUCache.get(2); // 返回 -1(未找到) lFUCache.get(3); // 返回 3 // cache=[3,1], cnt(3)=2, cnt(1)=2 lFUCache.put(4, 4); // 去除鍵 1 ,1 和 3 的 cnt 相同,但 1 最久未使用 // cache=[4,3], cnt(4)=1, cnt(3)=2 lFUCache.get(1); // 返回 -1(未找到) lFUCache.get(3); // 返回 3 // cache=[3,4], cnt(4)=1, cnt(3)=3 lFUCache.get(4); // 返回 4 // cache=[3,4], cnt(4)=2, cnt(3)=3
提示:
0 <= capacity, key, value <= 104
- 最多呼叫
105
次get
和put
方法
解題思路
對比 Medium | LeetCode 146. LRU 快取機制 | HashMap+雙向連結串列 | LinkedHasp 有一些共同點。但是還有有很大的不同。
首先借鑑前題的思路, 為了保證get的複雜度是O(1), 這個時候需要使用一個HashMap去儲存key和value的值。
這道題的關鍵是要動態的更新訪問次數, 然後在快取滿需要替換時, 換掉訪問次數最少的。所以對此問題展開了如下的分析。
思路一
為了找到訪問次數最少的, 第一反應是使用優先佇列的方式。因為優先佇列可以在O(1)時間彈出訪問最少的節點。
但是存在以下兩個問題:
1、優先佇列可以找到訪問最少的節點, 但是彈出過後, 還有做一個堆調整的過程。這個複雜度是O(logn)的
2、Put一個新key的時候, 訪問次數是1, 新增到優先佇列時, 需要從堆底逐漸將這個節點調整到堆頂, 這個複雜度是O(logn)
3、當訪問某個非堆頂節點, 需要修改這個節點的訪問次數, 然後重新調整它在優先佇列的位置時, 是沒有操作辦法的。優先佇列既不會自動調整, 我們也沒有辦法將這個點刪除掉再插入調整。
上面第3個問題是致命的, 不僅僅是複雜度不達標, 更是最基本的實現都無法滿足。
所以此題不能使用優先佇列, 應當使用的是TreeSet方法。這是一個有序的Set, 底層採用紅黑樹的儲存結構。刪除, 新增的操作均是O(logn)。所以上述的第三個問題可以使用TreeSet解決。1, 2只是複雜度沒有達到要求。
同時此題要求 在相同次數下, 依據LRU演算法進行淘汰。所以這個時候可以在節點中新增一個time欄位, 並且在快取當中維持一個全域性的time。每個訪問某個節點時, 將當前訪問的節點的time依據全域性的time進行調整。
public class LFUCache {
static class LFUNode implements Comparable {
int visitCount;
int key;
int value;
int time; // 最近的訪問時間
public LFUNode(int key, int value, int time) {
this.key = key;
this.value = value;
this.time = time;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
} else {
return this.time - node.time;
}
}
}
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
private final HashMap<Integer, LFUNode> map = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int time; // 全域性的時間欄位, 每次get put 此欄位自增
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
this.time = 0;
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
int curTime = refreshCurrentTime();
LFUNode node = map.get(key);
if (node != null) {
// Cache命中
// 先更新值
queue.remove(node);
node.value = value;
node.time = curTime;
// 更新訪問次數, 由於紅黑樹當中修改節點值並不會修改順序
// 所以需要將此節點從紅黑樹中刪除再次插入
node.visitCount++;
queue.add(node);
return;
}
// Cache沒有命中
if (curCapacity == MAX_CAPACITY) {
LFUNode popNode = queue.pollFirst();
map.remove(popNode.key);
curCapacity--;
}
LFUNode newNode = new LFUNode(key, value, curTime);
newNode.visitCount = 1;
queue.add(newNode);
this.curCapacity++;
map.put(key, newNode);
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
int curTime = refreshCurrentTime();
LFUNode node = map.get(key);
if (node != null) {
// Cache命中
// 更新訪問次數, 由於紅黑樹當中修改節點值並不會修改順序
// 所以需要將此節點從紅黑樹中刪除再次插入
queue.remove(node);
// 必須先移除才能修改訪問次數, 如果先修改訪問次數再移除會移除失敗
node.visitCount++;
node.time = curTime;
// 這裡也是, 在add前必須將visitCount和time修改完, 然後add。add之後修改是不會改變順序的
queue.add(node);
return node.value;
}
return -1;
}
public int refreshCurrentTime() {
return ++this.time;
}
}
思路二
為了找到次數最少的, 還有一種辦法是將訪問次數通過HashMap儲存起來。以<Freq, List<Node>>的形式儲存。因為有多個節點具有相同的訪問次數。並且要求相同次數下, 需要使用LRU進行替換。 所以借鑑前一題當中的LRU的演算法, 這裡需要使用一個雙向連結串列來儲存相同次數的節點
在進行get和put時, 訪問次數會發生變化, 需要將原節點將其所在的雙向連結串列當中刪除, 並且更改訪問次數, 新增到新的雙向連結串列的頭部。
在訪問空間滿是需要淘汰時, 可以記錄一個全域性的minFreq來表示<Freq, List<Node>>的map裡雙向連結串列非空的最小的Freq。這個minFreq的維護策略是, 當put新的key(新增)時, minFreq = 1。當put更新已有的key, 或者get命中時, 將此節點刪除的時候, 判斷刪除後雙向連結串列是否為空。如果為空了並且minFreq和當前的訪問次數相同, 則將minFreq自增。然後在快取淘汰時, 由於要新增新節點, 所以minFreq必須變成1, 所以在淘汰快取時, 不需要對minFreq做調整。
public class LFUCache {
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
// keyMap 用來儲存key和value的對映
private final HashMap<Integer, LFUNode> keyMap = new HashMap<>();
// countMap 用來儲存訪問次數 的 節點, 主要用來做淘汰策略
private final HashMap<Integer, Deque<LFUNode>> countMap = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int minFreq;
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
}
// incVisitCount 訪問次數自增, 並且調整此節點在雙向連結串列的位置
public void incVisitCount(LFUNode node) {
int visitCount = node.visitCount;
Deque<LFUNode> nodes = countMap.get(visitCount);
nodes.remove(node);
// 更新訪問次數時, 需要更新最小的訪問次數
if (nodes.size() == 0 && minFreq == visitCount) {
minFreq++;
}
node.visitCount = ++visitCount;
nodes = countMap.get(visitCount);
if (nodes == null) {
nodes = new LinkedList<>();
countMap.put(visitCount, nodes);
}
nodes.addFirst(node);
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
LFUNode node = keyMap.get(key);
if (node != null) {
// Cache命中
node.value = value;
incVisitCount(node);
return;
}
// Cache未命中
if (curCapacity == MAX_CAPACITY) {
// Cache已滿, 淘汰訪問次數最少的, 並且最久未訪問的
LFUNode popNode = countMap.get(minFreq).removeLast();
keyMap.remove(popNode.key);
curCapacity--;
}
// 新增新的節點
LFUNode newNode = new LFUNode(key, value);
minFreq = 1;
keyMap.put(key, newNode);
Deque<LFUNode> sameCntNodes = countMap.get(1);
if (sameCntNodes == null) {
sameCntNodes = new LinkedList<>();
countMap.put(1, sameCntNodes);
}
sameCntNodes.addFirst(newNode);
curCapacity++;
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
LFUNode node = keyMap.get(key);
if (node != null) {
incVisitCount(node);
return node.value;
}
return -1;
}
}
class LFUNode implements Comparable {
int visitCount;
int key;
int value;
public LFUNode(int key, int value) {
this.key = key;
this.value = value;
this.visitCount = 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
}
return 0;
}
}
上述答案沒有問題, 但是LeetCode提交程式碼時, 顯示在時間上只打敗了5%的對手, 檢查程式碼才發現LinkedList的remove(Object o)方法是通過遍歷的方式找到key相等的節點然後刪除的。所以時間複雜度很高。所以要想取得一個高效的結果, 需要自己寫一個雙向連結串列。
public class LFUCache {
static class LFUNode implements Comparable {
int visitCount;
int key;
int value;
LFUNode prev;
LFUNode next;
public LFUNode() {
}
public LFUNode(int key, int value) {
this.key = key;
this.value = value;
this.visitCount = 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
}
return 0;
}
}
static class DoubleLinkedList {
private LFUNode head, tail;
public DoubleLinkedList() {
head = new LFUNode();
tail = new LFUNode();
head.next = tail;
}
public void unlink(LFUNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
public void addToHead(LFUNode node) {
node.prev = head;
node.next = head.next;
head.next = node;
node.next.prev = node;
}
public boolean isEmpty() {
return tail == head.next;
}
public LFUNode removeLast() {
LFUNode toDeletedNode = tail.prev;
unlink(toDeletedNode);
return toDeletedNode;
}
}
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
// keyMap 用來儲存key和value的對映
private final HashMap<Integer, LFUNode> keyMap = new HashMap<>();
// countMap 用來儲存訪問次數 的 節點, 主要用來做淘汰策略
private final HashMap<Integer, DoubleLinkedList> countMap = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int minFreq;
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
}
public void incVisitCount(LFUNode node) {
int visitCount = node.visitCount;
DoubleLinkedList nodes = countMap.get(visitCount);
nodes.unlink(node);
// 更新訪問次數時, 需要更新最小的訪問次數
if (nodes.isEmpty() && minFreq == visitCount) {
minFreq++;
}
node.visitCount = ++visitCount;
nodes = countMap.get(visitCount);
if (nodes == null) {
nodes = new DoubleLinkedList();
countMap.put(visitCount, nodes);
}
nodes.addToHead(node);
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
LFUNode node = keyMap.get(key);
if (node != null) {
// Cache命中
node.value = value;
incVisitCount(node);
return;
}
// Cache未命中
if (curCapacity == MAX_CAPACITY) {
// Cache已滿, 淘汰訪問次數最少的, 並且最久未訪問的
LFUNode popNode = countMap.get(minFreq).removeLast();
keyMap.remove(popNode.key);
curCapacity--;
}
LFUNode newNode = new LFUNode(key, value);
minFreq = 1;
keyMap.put(key, newNode);
DoubleLinkedList sameCntNodes = countMap.get(1);
if (sameCntNodes == null) {
sameCntNodes = new DoubleLinkedList();
countMap.put(1, sameCntNodes);
}
sameCntNodes.addToHead(newNode);
curCapacity++;
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
LFUNode node = keyMap.get(key);
if (node != null) {
incVisitCount(node);
return node.value;
}
return -1;
}
}