好吧,有人可能以爲我標題黨了,但我想告訴大家的是,前陣子面試確實掛在了 RLU 緩存算法的設計上了。當時作題的時候,本身想的太多了,感受設計一個 LRU(Least recently used) 緩存算法,不會這麼簡單啊,因而理解錯了題意(我也是服了,還能理解成這樣,,,,),本身一波操做寫了好多代碼,後來卡住了,再去仔細看題,發現本身應該是理解錯了,就是這麼簡單,設計一個 LRU 緩存算法。node
不過這時時間就很緊了,按道理若是你真的對這個算法很熟,十分鐘就能寫出來了,可是,本身雖然理解 LRU 緩存算法的思想,也知道具體步驟,但以前卻歷來沒有去動手寫過,致使在寫的時候,很是不熟練,也就是說,你感受本身會 和你可以用代碼完美着寫出來是徹底不是一回事,因此在此提醒各位,若是能夠,必定要本身用代碼實現一遍本身自覺得會的東西。千萬不要以爲本身理解了思想,就不用去寫代碼了,獨自擼一遍代碼,纔是真的理解了。面試
今天我帶你們用代碼來實現一遍 LRU 緩存算法,之後你在遇到這類型的題,保證你完美秒殺它。算法
設計並實現最不常用(LFU)緩存的數據結構。它應該支持如下操做:get 和 put。緩存
get(key) - 若是鍵存在於緩存中,則獲取鍵的值(老是正數),不然返回 -1。數據結構
put(key, value) - 若是鍵不存在,請設置或插入值。當緩存達到其容量時,它應該在插入新項目以前,
使最不常用的項目無效。在此問題中,當存在平局(即兩個或更多個鍵具備相同使用頻率)時,
最近最少使用的鍵將被去除。this
進階:.net
你是否能夠在 O(1) 時間複雜度內執行兩項操做?設計
示例:code
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
咱們要刪的是最近最少使用的節點,一種比較容易想到的方法就是使用單鏈表這種數據結構來存儲了。當咱們進行 put 操做的時候,會出現如下幾種狀況:ci
一、若是要 put(key,value) 已經存在於鏈表之中了(根據key來判斷),那麼咱們須要把鏈表中久的數據刪除,而後把新的數據插入到鏈表的頭部。、
二、若是要 put(key,value) 的數據沒有存在於鏈表以後,咱們咱們須要判斷下緩存區是否已滿,若是滿的話,則把鏈表尾部的節點刪除,以後把新的數據插入到鏈表頭部。若是沒有滿的話,直接把數據插入鏈表頭部便可。
對於 get 操做,則會出現如下狀況
一、若是要 get(key) 的數據存在於鏈表中,則把 value 返回,而且把該節點刪除,刪除以後把它插入到鏈表的頭部。
二、若是要 get(key) 的數據不存在於鏈表以後,則直接返回 -1 便可。
大概的思路就是這樣,不要以爲很簡單,讓你手寫的話,十分鐘你不必定手寫的出來。具體的代碼,爲了避免影響閱讀,我在文章的最後面在放出來。
時間、空間複雜度分析
對於這種方法,put 和 get 都須要遍歷鏈表查找數據是否存在,因此時間複雜度爲 O(n)。空間複雜度爲 O(1)。
在實際的應用中,當咱們要去讀取一個數據的時候,會先判斷該數據是否存在於緩存器中,若是存在,則返回,若是不存在,則去別的地方查找該數據(例如磁盤),找到後在把該數據存放於緩存器中,在返回。
因此在實際的應用中,put 操做通常伴隨着 get 操做,也就是說,get 操做的次數是比較多的,並且命中率也是相對比較高的,進而 put 操做的次數是比較少的,咱們咱們是能夠考慮採用空間換時間的方式來加快咱們的 get 的操做的。
例如咱們能夠用一個額外哈希表(例如HashMap)來存放 key-value,這樣的話,咱們的 get 操做就能夠在 O(1) 的時間內尋找到目標節點,而且把 value 返回了。
然而,你們想一下,用了哈希表以後,get 操做真的可以在 O(1) 時間內完成嗎?
用了哈希表以後,雖然咱們可以在 O(1) 時間內找到目標元素,能夠,咱們還須要刪除該元素,而且把該元素插入到鏈表頭部啊,刪除一個元素,咱們是須要定位到這個元素的前驅的,而後定位到這個元素的前驅,是須要 O(n) 時間複雜度的。
最後的結果是,用了哈希表時候,最壞時間複雜度仍是 O(1),而空間複雜度也變爲了 O(n)。
咱們都已經可以在 O(1) 時間複雜度找到要刪除的節點了,之因此還得花 O(n) 時間複雜度才能刪除,主要是時間是花在了節點前驅的查找上,爲了解決這個問題,其實,咱們能夠把單鏈表換成雙鏈表,這樣的話,咱們就能夠很好着解決這個問題了,並且,換成雙鏈表以後,你會發現,它要比單鏈表的操做簡單多了。
因此咱們最後的方案是:雙鏈表 + 哈希表,採用這兩種數據結構的組合,咱們的 get 操做就能夠在 O(1) 時間複雜度內完成了。因爲 put 操做咱們要刪除的節點通常是尾部節點,因此咱們能夠用一個變量 tai 時刻記錄尾部節點的位置,這樣的話,咱們的 put 操做也能夠在 O(1) 時間內完成了。
具體代碼以下:
// 鏈表節點的定義 class LRUNode{ String key; Object value; LRUNode next; LRUNode pre; public LRUNode(String key, Object value) { this.key = key; this.value = value; } }
// LRU public class LRUCache { Map<String, LRUNode> map = new HashMap<>(); RLUNode head; RLUNode tail; // 緩存最大容量,咱們假設最大容量大於 1, // 固然,小於等於1的話須要多加一些判斷另行處理 int capacity; public RLUCache(int capacity) { this.capacity = capacity; } public void put(String key, Object value) { if (head == null) { head = new LRUNode(key, value); tail = head; map.put(key, head); } LRUNode node = map.get(key); if (node != null) { // 更新值 node.value = value; // 把他從鏈表刪除而且插入到頭結點 removeAndInsert(node); } else { LRUNode tmp = new LRUNode(key, value); // 若是會溢出 if (map.size() >= capacity) { // 先把它從哈希表中刪除 map.remove(tail); // 刪除尾部節點 tail = tail.pre; tail.next = null; } map.put(key, tmp); // 插入 tmp.next = head; head.pre = tmp; head = tmp; } } public Object get(String key) { LRUNode node = map.get(key); if (node != null) { // 把這個節點刪除並插入到頭結點 removeAndInsert(node); return node.value; } return null; } private void removeAndInsert(LRUNode node) { // 特殊狀況先判斷,例如該節點是頭結點或是尾部節點 if (node == head) { return; } else if (node == tail) { tail = node.pre; tail.next = null; } else { node.pre.next = node.next; node.next.pre = node.pre; } // 插入到頭結點 node.next = head; node.pre = null; head.pre = node; head = node; } }
這裏須要提醒的是,對於鏈表這種數據結構,頭結點和尾節點是兩個比較特殊的點,若是要刪除的節點是頭結點或者尾節點,咱們通常要先對他們進行處理。
這裏放一下單鏈表版本的吧
// 定義鏈表節點 class RLUNode{ String key; Object value; RLUNode next; public RLUNode(String key, Object value) { this.key = key; this.value = value; } } // 把名字寫錯了,把 LRU寫成了RLU public class RLUCache { RLUNode head; int size = 0;// 當前大小 int capacity = 0; // 最大容量 public RLUCache(int capacity) { this.capacity = capacity; } public Object get(String key) { RLUNode cur = head; RLUNode pre = head;// 指向要刪除節點的前驅 // 找到對應的節點,並把對應的節點放在鏈表頭部 // 先考慮特殊狀況 if(head == null) return null; if(cur.key.equals(key)) return cur.value; // 進行查找 cur = cur.next; while (cur != null) { if (cur.key.equals(key)) { break; } pre = cur; cur = cur.next; } // 表明沒找到了節點 if (cur == null) return null; // 進行刪除 pre.next = cur.next; // 刪除以後插入頭結點 cur.next = head; head = cur; return cur.value; } public void put(String key, Object value) { // 若是最大容量是 1,那就沒辦法了,,,,, if (capacity == 1) { head = new RLUNode(key, value); } RLUNode cur = head; RLUNode pre = head; // 先查看鏈表是否爲空 if (head == null) { head = new RLUNode(key, value); return; } // 先查看該節點是否存在 // 第一個節點比較特殊,先進行判斷 if (head.key.equals(key)) { head.value = value; return; } cur = cur.next; while (cur != null) { if (cur.key.equals(key)) { break; } pre = cur; cur = cur.next; } // 表明要插入的節點的 key 已存在,則進行 value 的更新 // 以及把它放到第一個節點去 if (cur != null) { cur.value = value; pre.next = cur.next; cur.next = head; head = cur; } else { // 先建立一個節點 RLUNode tmp = new RLUNode(key, value); // 該節點不存在,須要判斷插入後會不會溢出 if (size >= capacity) { // 直接把最後一個節點移除 cur = head; while (cur.next != null && cur.next.next != null) { cur = cur.next; } cur.next = null; tmp.next = head; head = tmp; } } } }
若是要時間,強烈建議本身手動實現一波。
最後推廣下個人公衆號:苦逼的碼農:戳我便可關注,文章都會首發於個人公衆號,期待各路英雄的關注交流。