460. LFU Cache

Description

Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get and put.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) - Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.

Follow up:
Could you do both operations in O(1) time complexity?

Example:

LFUCache cache = new LFUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.get(3);       // returns 3.
cache.put(4, 4);    // evicts key 1.
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

Solution

Two HashMap + DoublyLinkedList, O(1), S(capacity)

真的是難題啊杈笔,邏輯算很復雜的了,非常容易出bug糕非。
思路基于LRU Cache蒙具,為了支持O(1) evict lease frequently used element,需要有一個freqMap<Integer, DLL>朽肥,還需要一個int minFreq禁筏,這樣就夠了。

一個比較tricky的地方是衡招,minFreq的更新好像很麻煩篱昔。但實際上沒有想象中的復雜,minFreq的更新是有跡可循的始腾。

  • 當update node時州刽,要判斷node是否已經(jīng)存在:
    • 如果node存在,node.freq++窘茁,如果node.oldFreq == minFreq怀伦,可能需要刪除minFreqList,這時只需判斷minFreqList在更新之后是否為空即可山林。
      • 不為空:minFreq保持不變
      • 為空:minFreq++即可,因為node.newFreq = node.oldFreq + 1邢羔,且它一定變成了新的minFreq
    • 如果node不存在驼抹,那么需要新建node,node.freq = 1拜鹤。minFreq自然就變成1了框冀,因為1是可能出現(xiàn)的最小freq!
  • 當evict node時敏簿,感覺minFreq的更新會變得復雜是不是明也,因為可能需要找到secondMinFreq對嗎宣虾?但實際情況很簡單,因為evict node一定是跟initialize node成對兒出現(xiàn)的(只有在達到capacity時)温数,所以可以回歸到上面的update node node為空的情況绣硝,minFreq一定會變成1。刺不刺激撑刺?

注意以下幾個坑:

  • Node或List的創(chuàng)建或銷毀鹉胖,一定要跟Map的更新同步操作!否則會出現(xiàn)不一致够傍。
  • 注意處理capacity == 0的情況
class LFUCache {

    class Node {
        Node prev, next;
        int key, val, freq;
        
        public Node(int k, int v) {
            key = k;
            val = v;
            freq = 1;   // initial freq is always 1
        }
    }
    
    class DLL {
        Node head, tail;
        int size;
        
        public DLL() {
            head = new Node(0, 0);
            tail = new Node(0, 0);
            head.next = tail;
            tail.prev = head;
        }
        // add node after head
        public void add(Node node) {
            head.next.prev = node;
            node.next = head.next;
            node.prev = head;
            head.next = node;
            ++size;
        }
        
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
            --size;
        }
        
        public Node removeLast() {
            Node last = tail.prev;
            remove(last);
            return last;
        }
        
        public boolean isEmpty() {
            return size == 0;
        }
    }
    
    private int capacity;
    private int minFreq;
    private Map<Integer, Node> nodeMap;
    private Map<Integer, DLL> freqMap;
    
    public LFUCache(int capacity) {
        this.capacity = capacity;
        minFreq = 0;    // initial minFreq is 0 of course
        nodeMap = new HashMap<>();
        freqMap = new HashMap<>();
    }
    
    public int get(int key) {
        if (!nodeMap.containsKey(key)) {
            return -1;
        }
        Node node = nodeMap.get(key);
        update(node);
        return node.val;
    }
    
    public void put(int key, int val) {
        if (capacity < 1) { // if capacity is 0, we shouldn't make any change
            return;
        }
        
        if (nodeMap.containsKey(key)) {
            Node node = nodeMap.get(key);
            node.val = val;
            update(node);
        } else {
            if (nodeMap.size() == capacity) {
                evict();
            }
            
            Node node = new Node(key, val);
            nodeMap.put(key, node);
            minFreq = 1;    // update minFreq to 1! Because curr node is a new one
            DLL newList = freqMap.getOrDefault(node.freq, new DLL());
            freqMap.put(node.freq, newList);
            newList.add(node);
        }
    }
    // handle the tricky logic to move node from oldFreqList to newFreqList
    // don't forget to update minFreq if old freq list is the minFreq and empty
    private void update(Node node) {
        // deal with old freq list
        int oldFreq = node.freq;
        DLL oldList = freqMap.get(oldFreq);
        oldList.remove(node);
        if (oldList.isEmpty()) {    // evict empty freq list
            freqMap.remove(oldFreq);
            if (oldFreq == minFreq) {   // if the removed freq list is the min
                ++minFreq;  // just increase the minFreq because freqs are consequtive!
            }
        }
        // deal with new freq list
        ++node.freq;
        DLL newList = freqMap.getOrDefault(node.freq, new DLL());
        freqMap.put(node.freq, newList);
        newList.add(node);
    }
    // evict lease frequently used node
    private void evict() {
        Node last = freqMap.get(minFreq).removeLast();
        nodeMap.remove(last.key);
    }
}

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache obj = new LFUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

Three HashMap + LinkedHashSet, O(1), S(capacity)

在Java中可以利用LinkedHashSet來當做DoublyLinkedList甫菠。

A LinkedHashSet is an ordered version of HashSet that maintains a doubly-linked List across all elements. When the iteration order is needed to be maintained this class in used. When iterating through a HashSet the order is unpredictable, while a LinkedHashSet lets us iterate through the elements in the order in which they were inserted.when cycling through LinkedHashSet using an iterator, the elements will be returned in the order in which they were inserted.

LinkedHashSet內(nèi)部應該是一個HashMap + DoublyLinkedList,其特性為:

  • 包含HashSet的一切特性:unique value冕屯,O(1) get, put
  • remains insertion order:用iterator遍歷時會按照插入順序從老到新寂诱。

下面的代碼就是用LinkedHashSet作為DLL的實現(xiàn),思路跟上面的方案是一致的安聘。

class LFUCache {
    private int capacity;
    private int minFreq;
    private Map<Integer, Integer> valMap;
    private Map<Integer, Integer> freqMap;
    private Map<Integer, Set<Integer>> freqSetMap;
    
    public LFUCache(int capacity) {
        this.capacity = capacity;
        minFreq = 0;
        valMap = new HashMap<>();
        freqMap = new HashMap<>();
        freqSetMap = new HashMap<>();
    }
    
    public int get(int key) {
        if (!valMap.containsKey(key)) {
            return -1;
        }
        
        update(key);
        return valMap.get(key);
    }
    
    public void put(int key, int val) {
        if (capacity < 1) {
            return;
        }
        
        if (valMap.containsKey(key)) {
            valMap.put(key, val);
            update(key);
        } else {
            if (valMap.size() == capacity) {
                evict();
            }
            
            valMap.put(key, val);
            freqMap.put(key, 1);
            freqSetMap.put(1, freqSetMap.getOrDefault(1, new LinkedHashSet<>()));
            freqSetMap.get(1).add(key);
            minFreq = 1;
        }
    }
    
    private void update(int key) {
        int oldFreq = freqMap.get(key);
        int newFreq = oldFreq + 1;
        freqMap.put(key, newFreq);
        
        Set<Integer> oldFreqSet = freqSetMap.get(oldFreq);
        oldFreqSet.remove(key);
        if (oldFreqSet.isEmpty()) {
            freqSetMap.remove(oldFreq);
            if (oldFreq == minFreq) {
                ++minFreq;
            }
        }
        
        Set<Integer> newFreqSet = freqSetMap.getOrDefault(newFreq, new LinkedHashSet<>());
        freqSetMap.put(newFreq, newFreqSet);
        newFreqSet.add(key);
    }
    
    private void evict() {
        Set<Integer> minFreqSet = freqSetMap.get(minFreq);
        int lastKey = minFreqSet.iterator().next();
        minFreqSet.remove(lastKey);
        valMap.remove(lastKey);
        freqMap.remove(lastKey);
        
        if (minFreqSet.isEmpty()) {
            freqSetMap.remove(minFreq); // minFreq should be set to 1 later
        }
    }
}

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache obj = new LFUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痰洒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搞挣,更是在濱河造成了極大的恐慌带迟,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件囱桨,死亡現(xiàn)場離奇詭異仓犬,居然都是意外死亡,警方通過查閱死者的電腦和手機舍肠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門搀继,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翠语,你說我怎么就攤上這事叽躯。” “怎么了肌括?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵点骑,是天一觀的道長。 經(jīng)常有香客問我谍夭,道長黑滴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任紧索,我火速辦了婚禮袁辈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘珠漂。我一直安慰自己晚缩,他們只是感情好尾膊,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荞彼,像睡著了一般冈敛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卿泽,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天莺债,我揣著相機與錄音,去河邊找鬼签夭。 笑死齐邦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的第租。 我是一名探鬼主播措拇,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼慎宾!你這毒婦竟也來了丐吓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤趟据,失蹤者是張志新(化名)和其女友劉穎券犁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汹碱,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡粘衬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了咳促。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稚新。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖跪腹,靈堂內(nèi)的尸體忽然破棺而出褂删,到底是詐尸還是另有隱情,我是刑警寧澤冲茸,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布屯阀,位于F島的核電站,受9級特大地震影響轴术,放射性物質(zhì)發(fā)生泄漏蹲盘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一膳音、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铃诬,春花似錦祭陷、人聲如沸苍凛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽醇蝴。三九已至,卻和暖如春想罕,著一層夾襖步出監(jiān)牢的瞬間悠栓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工按价, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惭适,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓楼镐,卻偏偏與公主長得像癞志,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子框产,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內(nèi)容