1. 程式人生 > 其它 >Hard | LeetCode 460. LFU 快取 | 設計(HashMap+雙向連結串列)(HashMap+TreeSet)

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) - 如果鍵已存在,則變更其值;如果鍵不存在,請插入鍵值對。當快取達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最近最久未使用 的鍵。

注意「項的使用次數」就是自插入該項以來對其呼叫 getput 函式的次數之和。使用次數會在對應項被移除後置為 0 。

為了確定最不常使用的鍵,可以為快取中的每個鍵維護一個 使用計數器 。使用計數最小的鍵是最久未使用的鍵。

當一個鍵首次插入到快取中時,它的使用計數器被設定為 1 (由於 put 操作)。對快取中的鍵執行 getput 操作,使用計數器的值將會遞增。

示例:

輸入:
["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
  • 最多呼叫 105getput 方法

解題思路

對比 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;
    }
}