• 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;
        }
    }
    
  • 相关阅读:
    [ZJOI2010]count 数字计数
    小雄数
    简单筛法函数
    [Noip模拟题]lucky
    欧拉线筛
    Intern Day78
    CodeForces1360C
    CodeForces1373B
    Intern Day78
    Intern Day78
  • 原文地址:https://www.cnblogs.com/chenrj97/p/15359633.html
Copyright © 2020-2023  润新知