LeetCode演算法題解:LFU Cache
原題:https://leetcode.com/problems/lfu-cache/?tab=Description
題目要求
設計並實現一個資料結構,滿足LFU (Least Frequently Used) Cache特性,實現兩種操作:get和put
- get(key):返回指定key對應的value,如果指定key在cache中不存在,返回-1
- put(key, value):設定指定key的value,如果key不存在,則插入該key-value對。如果cache空間已滿,則將最少使用的key-value對移除,如果存在多個key-value對的使用次數相同,則將上次訪問時間最早的key-value對移除。
public class LFUCache {
public LFUCache(int capacity) {
}
public int get(int key) {
}
public void put(int key, int value) {
}
}
進階要求
以O(1)時間複雜度實現get和put操作
思路解析
這道題的要求可以分解成兩部分
- 實現一個key-value儲存結構,能夠通過key快速找到對應的value
- 在cache已滿時,快速定位到訪問次數最低且上次訪問時間最早的key,將其移除
上述兩種計算都應以O(1)的時間複雜度實現。
對於第1點要求,毫無爭議地應使用雜湊表來實現,雜湊表的插入時間複雜度永遠是O(1),在不發生雜湊衝突的前提下,查詢的時間複雜度也是O(1)。這裡我們可以偷個懶,直接把HashMap拿來用。
對於第2點要求,最基本思路是記錄每個key的訪問次數和上次訪問時間,在cache已滿時找到訪問次數最少的key,如果有多個key訪問次數一樣,再去找這些key裡上次訪問時間最早的一個進行移除。
很顯然,通過遍歷去找訪問次數最少的key是不行的,這樣時間複雜度就是O(n)了。我們可以考慮構建一個連結串列,確保訪問次數最少的key位於連結串列的尾部:
設定每次新插入的key的訪問次數為0,那麼插入時只需要將key置於連結串列的尾部,同時在移除key時只要移除連結串列尾部的key就行了,這樣插入和移除的時間複雜度都是O(1)了。
然而,可能存在多個key的訪問次數一樣的情況,這種情況下不得不去遍歷這些key,找到上次訪問時間最早的一個。這樣一來,時間複雜度就超過了O(1)。
對此,我們可以考慮把結構改成兩層連結串列:
外層連結串列的每個節點代表一組擁有同樣訪問次數的key,每個節點自身也是一個連結串列,內層連結串列確保上次訪問時間最早的key位於內層連結串列的尾部。
在這一資料結構下,我們在插入key時判斷外層連結串列尾部元素的freq是否為0,如果是,將key插入該內層連結串列的頭部,如果否,生成一個只包含key的外層連結串列,插入到外層連結串列的尾部。在訪問key時,將該key移動到外層連結串列的下一個節點的頭部。這樣一來,在移除key時,只需要移除外層連結串列尾部元素的尾部元素即可,插入、訪問、移除的時間複雜度都是O(1)。
程式碼解析
定義內層連結串列的元素物件Node:
private static class Node {
int key;
int value;
int frequency = 0; //訪問次數
Node next; //下一元素
Node prev; //前一元素
NodeQueue nq; //所屬的外層連結串列元素
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
定義外層連結串列的元素物件NodeQueue:
private static class NodeQueue {
NodeQueue next; //下一元素
NodeQueue prev; //前一元素
Node tail; //尾部Node
Node head; //頭部Node
public NodeQueue(NodeQueue next, NodeQueue prev, Node tail, Node head) {
this.next = next;
this.prev = prev;
this.tail = tail;
this.head = head;
}
}
定義LFU Cache:
import java.util.HashMap;
public class LFUCache {
private NodeQueue tail; //連結串列尾部的NodeQueue
private int capacity; //容量
private HashMap<Integer, Node> map; //儲存key-value對的HashMap
//構造方法
public LFUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<Integer, Node>(capacity);
}
}
接下來,實現整個資料結構中最關鍵的演算法:將Node右移
private void oneStepUp(Node n) {
n.frequency++; //訪問次數+1
boolean singleNodeQ = false; //為true時,代表此NodeQueue中只有一個Node元素
if(n.nq.head == n.nq.tail)
singleNodeQ = true;
if(n.nq.next != null) {
if(n.nq.next.tail.frequency == n.frequency) {
//右側NodeQueue的訪問次數與Node當前訪問次數一樣,將此Node置於右側NodeQueue的頭部
removeNode(n); //從當前NodeQueue中刪除Node
//把Node插入到右側NodeQueue的頭部
n.prev = n.nq.next.head;
n.nq.next.head.next = n;
n.nq.next.head = n;
n.nq = n.nq.next;
} else if(n.nq.next.tail.frequency > n.frequency) {
//右側NodeQueue的訪問次數大於Node當前訪問次數,則需要在兩個NodeQueue之間插入一個新的NodeQueue
if(!singleNodeQ) {
removeNode(n);
NodeQueue nnq = new NodeQueue(n.nq.next, n.nq, n, n);
n.nq.next.prev = nnq;
n.nq.next = nnq;
n.nq = nnq;
}
//如果當前NodeQueue中只有一個Node,那麼其實不需要任何額外操作了
}
} else {
//此NodeQueue的next == null,說明此NodeQueue已經位於外層連結串列頭部了,這時候需要往外側連結串列頭部插入一個新的NodeQueue
if(!singleNodeQ) {
removeNode(n);
NodeQueue nnq = new NodeQueue(null, n.nq, n, n);
n.nq.next = nnq;
n.nq = nnq;
}
//同樣地,如果當前NodeQueue中只有一個Node,不需要任何額外操作
}
}
移除Node的方法:
private Node removeNode(Node n) {
//如果NodeQueue中只有一個Node,那麼移除整個NodeQueue
if(n.nq.head == n.nq.tail) {
removeNQ(n.nq);
return n;
}
if(n.prev != null)
n.prev.next = n.next;
if(n.next != null)
n.next.prev = n.prev;
if(n.nq.head == n)
n.nq.head = n.prev;
if(n.nq.tail == n)
n.nq.tail = n.next;
n.prev = null;
n.next = null;
return n;
}
private void removeNQ(NodeQueue nq) {
if(nq.prev != null)
nq.prev.next = nq.next;
if(nq.next != null)
nq.next.prev = nq.prev;
if(this.tail == nq)
this.tail = nq.next;
}
接下來實現get和put方法:
get方法非常簡單了,到HashMap中拿到key對應的Node,並且將該Node右移:
public int get(int key) {
Node n = map.get(key);
if(n == null)
return -1;
oneStepUp(n);
return n.value;
}
put方法:
public void put(int key, int value) {
if(capacity == 0)
return;
Node cn = map.get(key);
//key已存在的情況下,更新value值,並將Node右移
if(cn != null) {
cn.value = value;
oneStepUp(cn);
return;
}
//cache已滿的情況下,把外層連結串列尾部的內層連結串列的尾部Node移除
if(map.size() == capacity) {
map.remove(removeNode(this.tail.tail).key);
}
//插入新的Node
Node n = new Node(key, value);
if(this.tail == null) {
//tail為null說明此時cache中沒有元素,直接把Node封裝到NodeQueue里加入
NodeQueue nq = new NodeQueue(null, null, n, n);
this.tail = nq;
n.nq = nq;
} else if(this.tail.tail.frequency == 0) {
//外層連結串列尾部元素的訪問次數是0,那麼將Node加入到外層連結串列尾部元素的頭部
n.prev = this.tail.head;
this.tail.head.next = n;
n.nq = this.tail;
this.tail.head = n;
} else {
//外層連結串列尾部元素的訪問次數不是0,那麼例項化一個只包含此Node的NodeQueue,加入外層連結串列尾部
NodeQueue nq = new NodeQueue(this.tail, null, n, n);
this.tail.prev = nq;
this.tail = nq;
n.nq = nq;
}
//最後把key和Node存入HashMap中
map.put(key, n);
}
執行結果:
還不錯,擊敗了99.81%的選手。不過LeetCode的程式碼執行時間統計不太穩定,這段程式碼跑起來的成績差不多在140~170ms之間。
相關文章
- LFU演算法實現演算法
- Redis中的LFU演算法Redis演算法
- 什麼是 LFU 演算法?演算法
- Leetcode 演算法題解系列 - 最小棧LeetCode演算法
- FIFO/LRU/LFU三種快取演算法快取演算法
- Leetcode LRU CacheLeetCode
- [LeetCode] LRU CacheLeetCode
- LeetCode演算法題LeetCode演算法
- Leetcode 題解演算法之動態規劃LeetCode演算法動態規劃
- LeetCode解題記錄(貪心演算法)(二)LeetCode演算法
- LeetCode解題記錄(貪心演算法)(一)LeetCode演算法
- 快取演算法(頁面置換演算法)-FIFO、LFU、LRU快取演算法
- leetcode演算法題解(Java版)-9-N皇后問題LeetCode演算法Java
- Leetcode-LRU CacheLeetCode
- LRU Cache leetcode javaLeetCodeJava
- leetcode排序專題演算法刷題LeetCode排序演算法
- leetcode演算法題解(Java版)-3-廣搜+HashMapLeetCode演算法JavaHashMap
- leetcode演算法題解(Java版)-14-第k小數問題LeetCode演算法Java
- LeetCode 146 [LRU Cache]LeetCode
- [leetcode 題解] 849LeetCode
- Leetcode 全套題解LeetCode
- 「LeetCode」全部題解LeetCode
- leetcode演算法資料結構題解---資料結構LeetCode演算法資料結構
- leetcode演算法題解(Java版)-12-中序遍歷LeetCode演算法Java
- 快取演算法:LRU、LFU、隨機替換等常見演算法簡介快取演算法隨機
- KMP演算法(Leetcode第28題)KMP演算法LeetCode
- 【LeetCode回溯演算法#07】子集問題I+II,鞏固解題模板並詳解回溯演算法中的去重問題LeetCode演算法
- LeetCode146:LRU CacheLeetCode
- leetcode題解(陣列問題)LeetCode陣列
- Leetcode題解1-50題LeetCode
- [Python手撕]LFUPython
- leetcode演算法題解(Java版)-7-迴圈連結串列LeetCode演算法Java
- Leetcode 565 & 240 題解LeetCode
- LeetCode 解題彙總LeetCode
- leetcode解題目錄LeetCode
- leetcode演算法題解(Java版)-16-動態規劃(單詞包含問題)LeetCode演算法Java動態規劃
- 每週刷個 leetcode 演算法題LeetCode演算法
- leetcode演算法熱題--兩樹之和LeetCode演算法