博客 / 詳情

返回

【每日一題】LFU 緩存

一個緩存結構需要實現如下功能:

void set(int key,int value):加入或者修改 key 對應的 value

int get(int key):查詢 key 對應的 value 值

但是緩存最多放 K 條記錄,如果新的 K + 1 條記錄需要加入,就需要根據策略刪掉一條記錄,然後才能把新記錄加入。

這個策略為:在緩存結構的 K 條記錄中,哪一個 key 從進入緩存結構的時刻開始,被調用 set 或者 get 次數最少,就刪掉這個key 的記錄;如果調用次數最少的 key 有多個,上次調用發送最早的 key 被刪除。

這個就是 LFU 緩存替換算法。實現這個結構,K 作為參數。

解法一:哈希表 + 有序表

緩存中的數據是 k,v 對,所以用 map 存儲數據。由於緩存數據需要根據:使用次數和使用時間進行淘汰,如果遍歷數據查找需要淘汰的數據,耗時比較高。因此需要維護一個有序結構,方便查找要淘汰的數據。

  • 有序數組:

    • 查找要淘汰數據的耗時為:O(1),刪除後需要移動數據,耗時為:O(K)
    • 每次操作都需要更新 count 和 time 並維護數組的有序,此時也需要查找並移動數據,耗時為為:O(K)
  • 有序鏈表:

    • 查找要淘汰數據的耗時為:O(1),(比如頭結點或者尾結點)
    • 更新操作時,查找對應節點的耗時為:O(K)
  • 有序表:

    • 查找並移除要淘汰的數據的耗時為:O(log K)
    • 更新操作的耗時時為:O(log K)
  • 小頂堆:

    • 查找並移除要淘汰的數據的耗時為:O(1),移除堆頂後需要堆化的耗時為:O(log K)。
    • 更新數據後,也需要堆化,耗時為:O(log K)。

時間複雜度:O(log K)

空間複雜度:O(K)

import java.util.*;

public class LFU1 {
    public static class Node implements Comparable<Node> {
        public int key;
        public int value;
        // 這個節點發生get或者set的次數總和
        public int count;
        // 最後一次操作的時間
        public int time;
        
        public Node(int key, int value, int count, int time) {
            this.key = key;
            this.value = value;
            this.count = count;
            this.time = time;
        }

        // 緩存淘汰優先級
        // 最少使用(count 越小越容易被淘汰)
        // count 相同,時間越早越容易被淘汰(time 越小越容易被淘汰)
        @Override
        public int compareTo(Node o) {
            return count == o.count ? Integer.compare(time, o.time) : Integer.compare(count, o.count);
        }

        @Override
        public String toString() {
            return "Node{" + "key=" + key + ", value=" + value + ", count=" + count + ", time=" + time + '}';
        }
    }

    public static class LFUCache {
        // 緩存過期優先級
        TreeSet<Node> set = new TreeSet<>();
        Map<Integer, Node> map = new HashMap<>();
        int capacity;
        int time = 0; // 用來計算緩存時間

        public LFUCache(int capacity) {
            this.capacity = Math.max(capacity, 0);
        }

        public Integer get(int key) {
            if (!map.containsKey(key)) {
                return null;
            }
            Node node = map.get(key);
            set(key, node.value);
            return node.value;
        }

        public void set(int key, int value) {
            this.time += 1;
            // 更新
            if (map.containsKey(key)) {
                Node node = map.get(key);
                // 刪除再插入(node 的count 和 time 變化了,TreeSet 認為是不同的數據)
                set.remove(node);
                node.time = this.time;
                node.count++;
                node.value = value;
                set.add(node);
                map.put(key, node);
                return;
            }

            // 新增
            // 如果內存滿了,淘汰一條舊數據
            if (this.capacity == this.map.size()) {
                remove();
            }
            Node node = new Node(key, value, 1, this.time);
            map.put(key, node);
            set.add(node);
        }

        public void remove() {
            if (map.size() == 0) {
                return;
            }
            Node node = set.first();
            map.remove(node.key);
            set.remove(node);
        }
    }
}

解法二:哈希表 + 小頂堆

將有序表更換為小頂堆。

刪除數據時,heap.pop()

更新數據後,堆化:heap.heapify(node)。更新數據使得 time 和 count 變大,因此只需要從 node 節點開始向下堆化

時間複雜度:O(log K)

空間複雜度:O(K)

import java.util.*;

public class LFU3 {
    public static class Node {
        public int key;
        public int value;
        // 這個節點發生get或者set的次數總和
        public int count;
        // 最後一次操作的時間
        public int time;

        public Node(int key, int value, int count, int time) {
            this.key = key;
            this.value = value;
            this.count = count;
            this.time = time;
        }

        @Override
        public String toString() {
            return "Node{" + "key=" + key + ", value=" + value + ", count=" + count + ", time=" + time + '}';
        }
    }


    public static class NodeComparator implements Comparator<Node> {

        // 緩存淘汰優先級
        // 最少使用(count 越小越容易被淘汰)
        // count 相同,時間越早越容易被淘汰(time 越小越容易被淘汰)
        @Override
        public int compare(Node o1, Node o2) {
            return o1.count == o2.count ? Integer.compare(o1.time, o2.time) : Integer.compare(o1.count, o2.count);
        }
    }

    public static class LFUCache {
        // 緩存過期優先級
        HeapGreater<Node> heap = new HeapGreater<>(new NodeComparator());


        Map<Integer, Node> map = new HashMap<>();
        int capacity;
        int time = 0; // 用來計算緩存時間

        public LFUCache(int capacity) {
            this.capacity = Math.max(capacity, 0);
        }

        public Integer get(int key) {
            if (!map.containsKey(key)) {
                return null;
            }
            Node node = map.get(key);
            set(key, node.value);
            return node.value;
        }

        public void set(int key, int value) {
            this.time += 1;
            // 更新
            if (map.containsKey(key)) {
                Node node = map.get(key);
                // 刪除再插入(node 的count 和 time 變化了,TreeSet 認為是不同的數據)
                node.time = this.time;
                node.count++;
                node.value = value;
                heap.heapify(node);
                map.put(key, node);
                return;
            }

            // 新增
            // 如果內存慢了,淘汰一條舊數據
            if (this.capacity == this.map.size()) {
                remove();
            }
            Node node = new Node(key, value, 1, this.time);
            map.put(key, node);
            heap.push(node);
        }

        public void remove() {
            if (map.size() == 0) {
                return;
            }
            Node node = heap.pop();
            map.remove(node.key);
        }
    }
}

加強堆的部分代碼

import java.util.*;

public class HeapGreater<T> {
    private ArrayList<T> heap;
    private HashMap<T, Integer> indexMap;
    private int heapSize;
    private Comparator<? super T> comp;

    public HeapGreater(Comparator<? super T> c) {
        heap = new ArrayList<>();
        indexMap = new HashMap<>();
        heapSize = 0;
        comp = c;
    }

    public void push(T obj) {
        heap.add(obj);
        indexMap.put(obj, heapSize);
        heapInsert(heapSize++);
    }

    public T pop() {
        T ans = heap.get(0);
        swap(0, heapSize - 1);
        indexMap.remove(ans);
        heap.remove(--heapSize);
        heapify(0);
        return ans;
    }

    private void heapInsert(int index) {
        while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
            swap(index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    public void heapify(T obj) {
        heapify(indexMap.get(obj));
    }

    private void heapify(int index) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            int best = left + 1 < heapSize && comp.compare(heap.get(left + 1), heap.get(left)) < 0 ? (left + 1) : left;
            best = comp.compare(heap.get(best), heap.get(index)) < 0 ? best : index;
            if (best == index) {
                break;
            }
            swap(best, index);
            index = best;
            left = index * 2 + 1;
        }
    }

    private void swap(int i, int j) {
        T o1 = heap.get(i);
        T o2 = heap.get(j);
        heap.set(i, o2);
        heap.set(j, o1);
        indexMap.put(o2, i);
        indexMap.put(o1, j);
    }
}

解法三:哈希表 + 二維雙向鏈表

如下圖就是一個二維雙向鏈表。桶與桶之間是雙向鏈表,桶內有一個雙向鏈表。桶內雙向鏈表上的數據擁有相同的操作次數,越靠近頭部的數據,操作時間越近(從鏈表頭部插入新數據,那麼要過期數據從尾部移除)。所以要過期一個數據,刪除操作數最小的桶(頭桶)中鏈表的尾節點。

下圖演示了桶之間鏈表和桶內鏈表的變化過程。

<font color=red>原則:缺少操作數的桶,就新建桶。節點移走後出現空桶,將空桶刪除。注意在這個過程中保持:桶之間的雙向鏈表正確連接。</font>

調用 get(B) :B 的count 從 3 變為 4,沒有操作數為 4 的桶,就新建桶,將新桶插入在桶 3 和 桶 5 之間。將節點 B 從桶 3 中移除,插入桶 4 中。

再次調用 get(B):B 的 count 從 4 變為 5,有操作數為 5 的桶,將節點 B 從桶 4 中移除,插入桶 5 (注意是頭節點)中。桶 4 移除節點 B 後,成為空桶,刪除空桶。將桶 3 與 桶 5 直接連接。

當需要刪除一條過期數據時,我們需要在頭桶中,刪除桶內鏈表的尾節點(尾結點是最早操作的數據)。

時間複雜度:O(1)

空間複雜度:O(K)

import java.util.*;

public class LFU2 {
    public static class Node {
        public int key;
        public int value;
        // 這個節點發生get或者set的次數總和
        public int count;
        // 雙向鏈表上一個節點
        public Node up;
        // 雙向鏈表下一個節點
        public Node down;

        public Node(int key, int value, int count) {
            this.key = key;
            this.value = value;
            this.count = count;
        }

        @Override
        public String toString() {
            return "Node{" + "key=" + key + ", value=" + value + ", count=" + count + '}';
        }
    }

    public static class NodeList {
        // 桶內鏈表的頭節點
        public Node head;
        // 桶內鏈表的尾節點
        public Node tail;
        // 桶之間的前一個桶
        public NodeList last;
        // 桶之間的後一個桶
        public NodeList next;


        public NodeList(Node node) {
            this.head = node;
            this.tail = node;
        }

        // 把一個新的節點加入這個桶,新的節點都放在頂端變成新的頭部
        public void addNodeFromHead(Node newHead) {
            newHead.down = head;
            head.up = newHead;
            head = newHead;
        }

        // 判斷這個桶是不是空的
        public boolean isEmpty() {
            return this.head == null;
        }

        // 刪除 node 節點並保證 node 的上下環境重新連接
        public void deleteNode(Node node) {
            if (head == tail) {
                this.head = null;
                this.tail = null;
            } else if (node == head) {
                head = node.down;
                head.up = null;
            } else if (node == tail) {
                tail = node.up;
                tail.down = null;
            } else {
                node.up.down = node.down;
                node.down.up = node.up;
            }
            node.up = null;
            node.down = null;
        }
    }


    // 總得緩存結構
    public static class LFUCache {
        // 緩存的大小限制
        public int capacity;
        // 緩存中目前有多少個節點
        public int size = 0;

        public Map<Integer, Node> map = new HashMap<>();
        // 表示節點 node在 哪個桶裏
        public Map<Node, NodeList> heads = new HashMap<>();
        // 整個桶鏈表的頭節點
        private NodeList headList;

        public LFUCache(int k) {
            this.capacity = k;
        }

        // removeNodeList:剛剛減少了一個節點的桶
        // 這個函數的功能是,判斷剛剛減少了一個節點的桶是不是已經空了。
        // 1)如果不空,什麼也不做
        // 2)如果空了,removeNodeList 還是整個緩存結構最左的桶 (headList)。
        // 刪掉這個桶的同時也要讓最左的桶變成removeNodeList的下一個。
        // 3)如果空了,removeNodeList不是整個緩存結構最左的桶(headList)。
        // 把這個桶刪除,並保證上一個的桶和下一個桶之間還是雙向鏈表的連接方式
        // 函數的返回值表示剛剛減少了一個節點的桶是不是已經空了,空了返回true;不空返回false
        private boolean modifyHeadList(NodeList removeNodeList) {
            if (!removeNodeList.isEmpty()) {
                return false;
            }
            if (removeNodeList == headList) {
                headList = removeNodeList.next;
                if (headList != null) {
                    headList.last = null;
                }
            } else {
                removeNodeList.last.next = removeNodeList.next;
                if (removeNodeList.next != null) {
                    removeNodeList.next.last = removeNodeList.last;
                }
            }
            return true;
        }

        // node 這個節點的次數 +1 了,這個節點原來在 oldNodeList 裏。
        // 把 node 從 oldNodeList 刪掉,然後放到次數 +1 的桶中
        // 整個過程既要保證桶之間仍然是雙向鏈表,也要保證節點之間仍然是雙向鏈表
        private void move(Node node, NodeList oldNodeList) {
            // 從 oldNodeList 中刪除
            oldNodeList.deleteNode(node);
            // preList表示次數 +1 的桶的前一個桶是誰
            // 如果 oldNodeList 刪掉 node 之後還有節點(oldNodeList 不需要刪除),oldNodeList 就是次數 +1 的桶的前一個桶
            // 如果 oldNodeList 刪掉 node 之後空了,oldNodeList是需要刪除的,所以次數 +1 的桶的前一個桶,是 oldNodeList 的前一個
            NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;


            NodeList nextList = oldNodeList.next;
            // 如果 oldNodeList 沒有後續了,那麼肯定需要新建一個桶來盛放 node
            if (nextList == null) {
                NodeList newList = new NodeList(node);
                if (preList != null) {
                    preList.next = newList;
                }
                newList.last = preList;
                if (headList == null) {
                    headList = newList;
                }
                heads.put(node, newList);
            } else {
                // oldNodeList 有後續了,並且 oldNodeList 的後續count == node.count,直接將 node 添加到這個桶裏。
                if (nextList.head.count == node.count) {
                    nextList.addNodeFromHead(node);
                    heads.put(node, nextList);
                } else {
                    // oldNodeList 的後續 count != node.count ,那麼需要新建一個桶來放 node。
                    NodeList newList = new NodeList(node);
                    if (preList != null) {
                        preList.next = newList;
                    }
                    newList.last = preList;
                    newList.next = nextList;
                    nextList.last = newList;
                    if (headList == nextList) {
                        headList = newList;
                    }
                    heads.put(node, newList);
                }
            }
        }

        public void set(int key, int value) {
            // 更新
            if (map.containsKey(key)) {
                Node node = map.get(key);
                node.count++;
                node.value = value;
                move(node, heads.get(node));
            } else {
                // 新增
                // 如果緩存已滿,需要淘汰一條舊數據
                if (size == capacity) {
                    // 從頭部新增,從尾部刪除。桶內雙向鏈表的順序,就是相同 count 的 time 值的排序。
                    // headList 是 count 值最小的桶,headList.tail 是 time 最小的節點。
                    Node node = headList.tail;
                    headList.deleteNode(node);
                    // 刪除數據節點後,維護一下 桶,看看是否需要刪除
                    modifyHeadList(headList);

                    map.remove(node.key);
                    heads.remove(node);
                    size--;
                }

                Node node = new Node(key, value, 1);
                if (headList == null) {
                    headList = new NodeList(node);
                } else {
                    // 新增節點 count = 1,如果 headList 的count 也是 1,直接將 node 加入 headList
                    if (headList.head.count == node.count) {
                        headList.addNodeFromHead(node);
                    } else {
                        // 如果如果 headList 的count 不是 1,需要新建一個 count = 1 的桶,作為 headList
                        NodeList newList = new NodeList(node);
                        newList.next = headList;
                        headList.last = newList;
                        headList = newList;
                    }
                }
                size++;
                map.put(key, node);
                heads.put(node, headList);
            }
        }

        public Integer get(int key) {
            if (!map.containsKey(key)) {
                return null;
            }

            Node node = map.get(key);
            node.count++;
            move(node, heads.get(node));
            return node.value;
        }
    }
}

對數器

        public static boolean check(LFU1.LFUCache lfu1, LFU2.LFUCache lfu2, LFU3.LFUCache lfu3) {
            if (lfu1.map.size() != lfu2.heads.size() || lfu1.map.size() != lfu3.map.size()) {
                return false;
            }

            for (int key : lfu1.map.keySet()) {
                if (!lfu2.map.containsKey(key) || !lfu3.map.containsKey(key)) {
                    return false;
                }
                Node node = lfu1.map.get(key);
                LFU2.Node node2 = lfu2.map.get(key);
                LFU2.Node node3 = lfu2.map.get(key);

                if (node.value != node2.value || node.count != node2.count || node.value != node3.value || node.count != node3.count) {
                    return false;
                }
            }
            return true;

        }

        public static void check() {
            LFU1.LFUCache lfu1 = new LFU1.LFUCache(3);
            LFU2.LFUCache lfu2 = new LFU2.LFUCache(3);
            LFU3.LFUCache lfu3 = new LFU3.LFUCache(3);

            for (int i = 0; i < 100000; i++) {
                int command = (int) (Math.random() * 3) % 2;
                int key = (int) (Math.random() * 10);
                int value = (int) (Math.random() * 10);

                if (command == 0) {
                    lfu1.set(key, value);
                    lfu2.set(key, value);
                    lfu3.set(key, value);
                } else {
                    lfu1.get(key);
                    lfu2.get(key);
                    lfu3.get(key);
                }
                if (!check(lfu1, lfu2, lfu3)) {
                    System.out.println("ERROR:res1:" + key);
                }
            }

            System.out.println("Nice");
        }

        public static void main(String[] args) {
            check();
        }

本文由mdnice多平台發佈

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.