• 3.1-3.2LFU&LRU算法


    3.1 手把手教你写LRU缓存淘汰算法

    3.1.1 LRU(Least Recently Used)算法介绍

    • LRU算法:是一种缓存淘汰策略,最近最少使用置换算法
    • **操作系统上的解释:**LRU是一种页面置换算法,在页面置换时,会根据每个页面上次被访问时的时间戳,比较当前内存页框中的所有页的时间戳,如果时间戳最远,则优先淘汰它,这是一种假设,假设以过去预测将来,过去很久没有用过的页在将来也大概率不会用到。
    • LRU置换算法的关键是按照访问时序来淘汰

    3.2.2 题目实例

    • 来看到leetcode.146题

    • 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
      实现 LRUCache 类:

    • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存

    • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。

    • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;

    • 如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该逐出最久未使用的关键字。

    • 首先题目要接收一个capacity参数作为缓存的最大容量,然后实现两个API,一个是put(key,val)get(key),方法获得key所对应的val,如果key不存在则返回-1

    • 注意,使用遍历暴力模拟的方式是过不了这道题的,必须使得get和put的时间复杂度为O(1)

    输入
    ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
    [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
    输出
    [null, null, null, 1, null, -1, null, -1, 3, 4]
    
    解释
    LRUCache lRUCache = new LRUCache(2);
    lRUCache.put(1, 1); // 缓存是 {1=1}
    lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
    lRUCache.get(1);    // 返回 1 {2=2, 1=1},因为访问了键1,所以2会提到队头
    lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3},会把队尾元素出队
    lRUCache.get(2);    // 返回 -1 (未找到)
    lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
    lRUCache.get(1);    // 返回 -1 (未找到)
    lRUCache.get(3);    // 返回 3
    lRUCache.get(4);    // 返回 4
    
    来源:力扣(LeetCode)
    链接:https://leetcode.cn/problems/lru-cache
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.1.3 算法设计

    • [1] 显然Cache中的元素必须具有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置
    • [2] 要在Cache中快速查找某个key是否存在并得到对应的val
    • [3] 每次访问cache中的某个key,需要将这个元素变为最近使用的,也就是说cache要支持在任意位置快速插入的和删除元素
    • 分析:哈希表查找快,但是数据无固定顺序,链表有顺序之分,插入删除快,但是在这之前定位节点需要时间复杂度为O(n)的查找,所以结合两者的特点,使用LinkedHashMap作为我们的数据结构,JAVA中已经有这个数据结构了,如果是其他语言的话还需要自己实现一下,其具体的结构如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQzH6Wrd-1661227302574)(./image/146-1.png)]

    • 哈希链表:可以根据key值快速查找到节点,在遍历时也可以根据插入的顺序来保证遍历顺序的唯一性。

      • 如果每次默认从链表尾部添加元素,那么显然越靠近尾部的元素就越是最近使用的,越靠近头部的元素就是越久未使用的

      • 对于某一个key,可以通过哈希表快速定位到链表中节点,从而取到对应的val

      • 链表显然是支持在任意位置的快速插入和删除的,只需要修改指针即可,只不过传统的链表无法安装索引快速访问某一个位置的元素,而这里借助了哈希表,可以通过key快速映射到任意一个链表节点,然后进行插入和删除

    • 我们首先从零开始实现一个双向链表

    //定义双链表的节点类,注意双链表的话是需要有两个指针,一个指针指向前面,一个指针指向后面
    class Node {
        public int key;
        public int val;
        public Node next,prev;
        public Node(int k,int v){
            this.key=k;
            this.val=v;
        }
    }
    //依靠Node类型构建双链表
    class DoubleList{
        //头尾的虚节点
        private Node head,tail;
        //链表的元素个数
        private int size;
    
        public DoubleList(){
            //初始化双向链表的数据
            head = new Node(0,0);
            tail = new Node(0,0);
            head.next=tail;
            //头结点没有上一个节点,下一个节点是尾结点
            tail.prev=head;
            //尾结点有上一个节点,没有下一个节点
            this.size = 0;
        }
    
        //在链表尾部添加节点x,时间复杂度为O(1)
        public void addList(Node x){
            //修改指针
            //x节点的上一个节点设置为tail的上一个节点
            x.prev = tail.prev;
            //x节点的下一个节点设置为tail
            x.next = tail;
            //tail节点的上一个节点的下一个节点属性设置为x
            tail.prev.next = x;
            //tail节点的上一个节点设置为x
            tail.prev =x;
            //链表长度+1
            this.size++;
        }
    
        //删除链表中的x节点(x一定存在)
        //由于是双链表而且给的是目标Node节点,时间复杂度是O(1)
        public void remove(Node x){
            //先处理之前节点的关系
            //使得x的上一个节点连接x的下一个节点
            x.prev.next = x.next;
            //使得x的下一个节点连接x的上一个节点
            x.next.prev = x.prev;
            this.size--;
        }
    
        //删除链表中的第一个节点,并返回该节点,时间复杂度为O(1)
        public Node removeFirstNode(){
            if(head.next == tail){
                return null;
            }
            Node x = head.next;
            //头节点的下一个节点指向第一个节点的下一个节点
            head.next = x.next;
            //第一个节点的下一个节点的上一个节点属性指向头结点
            x.next.prev = head;
            this.size--;
            return x;
        }
    
        //获取链表总长度
        public int getSize(){
            return this.size;
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • [问题1] 为什么要是双向链表呢而不是单链表呢?
      • 我们想要达到的效果是删除API的时间复杂度是O(1)而我们想要进行删除操作,那么就得到目标节点的时候,我们也希望能够以O(1)的方式得到其前驱节点,其前驱节点在单链表中是无法得到的,要得到的话只能通过O(n)的时间复杂度遍历得到,那么我们维护一个前驱指针来便于我们得到其前驱节点
    • 注意:我们所实现的双链表API只能从尾部插入,也就是靠尾部的数据是最近使用的,靠头部的数据是最久未使用的
    public class LRU {
        //key->Node(key,val)
        private HashMap<Integer, Node> map;
        //Node(k1,v1)<->Node(k2,v2)
        private DoubleList cache;
        //最大容量
        private int cap;
        public LRU(int cap){
            this.cap = cap;
            map = new HashMap<>();
            cache = new DoubleList();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 由于要同时维护一个双链表cache和一个哈希表map,很容易漏掉一些操作,比如删除某个key的时候,在cache中删除了对应的node,但是却忘记在map中删除key
    • 解决这种问题的有效方法是:在这两种数据结构之上提供一层抽象API,就是尽量让LRU的主方法getput避免直接操作mapcache的细节,简单来说,就是你不是会忘记这个忘记那个的嘛,那就把Cache和map的操作合并在一起就行
    public class LRU {
        //key->Node(key,val)
        private HashMap<Integer, Node> map;
        //Node(k1,v1)<->Node(k2,v2)
        private DoubleList cache;
        //最大容量
        private int cap;
        public LRU(int cap){
            this.cap = cap;
            map = new HashMap<>();
            cache = new DoubleList();
        }
        /*将某个key提升为最近使用*/
        private void makeRecently(int key){
            Node x= map.get(key);
            //先从链表中删除该节点
            cache.remove(x);
            //将该节点插入到表尾
            cache.addList(x);
        }
    
        /**
         * 添加最近使用的元素
         * @param key
         * @param val
         */
        private void addRecently(int key,int val){
            Node x = new Node(key,val);
            //链表尾部就是最近使用的元素
            cache.addList(x);
            //别忘了在map中添加key的映射
            map.put(key,x);
        }
    
        /**
         * 删除某一个key
         * @param key
         */
        private void deleteKey(int key){
            Node x = map.get(key);
            //从链表中删除该key所对应的节点
            cache.remove(x);
            //从map中删除
            map.remove(x);
        }
    
        /**
         * 删除最久没有使用的元素
         */
        public void removeLeastRecently(){
            //链表头部的第一个元素就是最久未使用的
            Node deleteNode = cache.removeFirstNode();
            //同时从map中删除它的key
            int key = deleteNode.key;
            map.remove(key);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 写完代码后来理一下思路

    • [问题2] 我们需要封装什么API?

      • 如果某个节点最近被使用过了,那么这个节点就应该被放到链表的尾部,也就是将指定节点移动到链表的尾部,这时候只需要操作cache
      • 如果需要将某个节点加入到Cache,那么这个节点应当是新节点,也就是将将指定节点移动到链表尾部并且在cache中添加数据
      • 如果需要置换某个节点,根据LRU算法的特点,也就是需要将链表头部节点删除,并且在cache中将其删除
      • 同时第三个API而言,我们还可以根据指定的key来删除指定的节点
    • [问题3] 既然哈希表中已经存了key,为什么链表中还要存key和val呢?只存val不就行了嘛?

      • 当缓存容量已满的时候,不仅要删除最后一个Node节点,还要把map中映射到该节点的key同时删除,而这个key只能由node来得到,如果node只存val的话就会造成没办法删除map中的键,造成错误
        /**
         * 外部取cache的值
         * @param key
         * @return
         */
        public int get(int key){
            if(!map.containsKey(key)){
                return -1;
            }
            //将该数据提升为最近使用的
            makeRecently(key);
            return map.get(key).val;
        }
    
        /**
         *
         * @param key
         * @param val
         */
        public void put(int key,int val){
            if(map.containsKey(key)){
                //如果有相同的key值,那么就删除旧的数据
                deleteKey(key);
                //新插入的数据为最近使用的数据
                this.addRecently(key,val);
                return;
            }
            //如果是新的数据,那么就需要淘汰一页后插入
            if(cap == cache.getSize()){
                //删除最久未使用的元素
                removeLeastRecently();
            }
            //添加最近使用的元素
            addRecently(key, val);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 了解了原理之后我们用Java内置的数据结构来完成这个事情
    import java.util.LinkedHashMap;
    
    class LRUCache {
        private LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
        int cap;
        public LRUCache(int capacity) {
            this.cap = capacity;
        }
    
        public int get(int key) {
            if(!cache.containsKey(key)){
                return -1;
            }
            //将key变为最近使用
            //1.将cache中的key对应的key在删除前保存下来
            int val = cache.get(key);
            //2.删除该key
            cache.remove(key);
            //3.将该key放到链表的尾部
            cache.put(key,val);
            return val;
        }
     
        public void put(int key, int value) {
            //首先检查cache中是否有该key,如果有,则覆盖
            if(cache.containsKey(key)){
                cache.remove(key);
                cache.put(key,value);
                return;
            }
            //如果cache中没有该key,则先检查是否需要置换
            if(cache.size() >= this.cap){
                //此时需要置换,需要删除链表的最后一个元素
                int oldKey  = cache.keySet().iterator().next();//取到链表的头部
                cache.remove(oldKey);
            }
            cache.put(key,value);
        }
    }
    
    /**
     * Your LRUCache object will be instantiated and called as such:
     * LRUCache obj = new LRUCache(capacity);
     * int param_1 = obj.get(key);
     * obj.put(key,value);
     */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    3.2 实现LFU(Least Frequently Used)缓存算法

    3.2.1 LFU算法介绍

    LFU算法:每次淘汰那些使用次数最少的数据,LRU算法相当于把数据按照时间进行排序,这个需求借助链表很自然就能够实现,一直从链表头部加入元素的话,就可以实现这个需求,其越接近头部的话就是越新的数据,越接近尾部的元素就越是旧的数据,进行缓存淘汰的时候只需要简单将尾部的元素淘汰即可。

    LFU算法相当于把数据按照访问频次进行排序,如果多个数据拥有相同的访问频次就应该删除最早插入的那个数据,也就是说LFU算法是淘汰访问频次最低的数据,如果访问频次最低的数据有多条,需要淘汰最旧的数据。

    3.2.1 算法描述

    请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。

    实现 LFUCache 类:

    LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
    int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
    void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除最近最久未使用的键。
    为了确定最不常使用的键,可以为缓存中的每个键维护一个使用计数器 。使用计数最小的键是最久未使用的键。

    当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。

    函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

    示例:

    输入:
    ["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 lfu = new LFUCache(2);
    lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
    lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
    lfu.get(1);      // 返回 1
                     // cache=[1,2], cnt(2)=1, cnt(1)=2
    lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                     // cache=[3,1], cnt(3)=1, cnt(1)=2
    lfu.get(2);      // 返回 -1(未找到)
    lfu.get(3);      // 返回 3
                     // cache=[3,1], cnt(3)=2, cnt(1)=2
    lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                     // cache=[4,3], cnt(4)=1, cnt(3)=2
    lfu.get(1);      // 返回 -1(未找到)
    lfu.get(3);      // 返回 3
                     // cache=[3,4], cnt(4)=1, cnt(3)=3
    lfu.get(4);      // 返回 4
                     // cache=[3,4], cnt(4)=2, cnt(3)=3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 要求你写一个类,接收一个capacity参数,实现get和put方法
    • get(key)方法去缓存中查询键key,如果 key已经存在,则返回key所对应的val,否则返回-1
    • put(key,val)方法插入或者修改缓存,如果key已经存在,则将它对应的值改为val,如果key不存在,则插入键值对(key,val)
    • 当容量达到capacity的时候,则应该在插入新的键值对之前,删除使用频次(下面使用freq来表示)最低的键值对,如果freq最低的键值有对多个,则删除最旧的那个

    3.2.3 思路分析

    • 首先需要计算从key到val的映射,这里我们使用一个HashMap keyToVal,就可以快速计算get(key)
    • 使用一个HashMap keyToFreq就可以快速操作key对应的freq
    • LFU算法核心
      • 需要freqkey的映射
      • freq最小的key删除,那就应该快速得到当前所有key最小的freq是多少。想要时间复杂度为O(1)的话,肯定不能进行遍历的,对于这种需求,我们可以维护一个临时变量(备忘录思想)minFreq来记录当前最小的freq
      • 可能有多个key拥有相同的freq,所以freqkey是一对多的关系,也就是freq对应一个key列表
      • 希望freq对应的key的列表是存在时序,便于快速查找并删除最旧的key
      • 希望能够快速删除key列表中的任意一个key,因为如果频次为freq的某个key被访问了,那么它的频次就应该变成freq+1,就应该从freq对应key列表中删除,加到freq+1对应的列表中
    • 于是设计出以下数据结构
        HashMap<Integer,Integer> keyToVal;//用于快速获取key=>val的值
        HashMap<Integer,Integer> keyToFreq;//用于快速获取每个值的访问频率
        HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;//用于形成一张表,该表中保存着每个频率所对应的key值
        //LinkedHashSet是链表和哈希集合的结合体,链表不能快速访问链表节点,需要遍历执行,无法达到我们想要快速删除目标值的目的
        //哈希集合中的元素无需,但是可以对元素快速的访问和删除
        int minFreq = 0;
        //临时维护的变量,用来记录最小的频次
        int cap = 0;
        //记录LFU缓存的最大容量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-41eUnghk-1661227302575)(./image/LFU-01.png)]

    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.LinkedHashSet;
    
    /**
     * 即将要维护KV表,KF表,FK表三个映射
     */
    class LFUCache {
        HashMap<Integer,Integer> keyToVal;//用于快速获取key=>val的值
        HashMap<Integer,Integer> keyToFreq;//用于快速获取每个值的访问频率
        HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;//用于形成一张表,该表中保存着每个频率所对应的key值
        //LinkedHashSet是链表和哈希集合的结合体,链表不能快速访问链表节点,需要遍历执行,无法达到我们想要快速删除目标值的目的
        //哈希集合中的元素无需,但是可以对元素快速的访问和删除
        int minFreq ;
        //临时维护的变量,用来记录最小的频次
        int cap ;
        //记录LFU缓存的最大容量
    
        public LFUCache(int capacity) {
            keyToVal = new HashMap<>();
            keyToFreq = new HashMap<>();
            freqToKeys = new HashMap<>();
            this.cap = capacity;
            this.minFreq = 0;//一开始还没有进行访问,故先初始化为0
        }
    
        public int get(int key) {
            //如果K-V表没有这个key,直接返回-1
            if(!keyToVal.containsKey(key)){
                return -1;
            }
            //否则就是有这个key,返回值之外,还需要对频率进行修改
            increase(key);
            return keyToVal.get(key);
        }
    
        public void put(int key, int value) {
            //根据算法思路图转换为代码
            if(this.cap<=0){
                return ;
            }
            //1.检查key是否存在,以KV表为准
            if(keyToVal.containsKey(key)){
                //当存在该key的时候
                keyToVal.put(key,value);
                //同时更新频次相关信息
                increase(key);
                return;
            }
    
            //2.如果key不存在,检查当前容量是否已经满了
            //FK KV KF
            if(keyToVal.size() >= this.cap){
                //如果已经满了,则需要置换
                /**LFU置换算法**/
                //首先获取freq最小的key列表
                LinkedHashSet<Integer> keys = freqToKeys.get(this.minFreq);
                //这些key都是待淘汰的,我们直接将最久没有使用过的key给它淘汰掉
                int oldest = keys.iterator().next();//这里获取的是链式集合头部的元素,越后面的就是越先插入的,淘汰最头部的
                //1.更新频次相关信息=>我们将FK表的信息给更新了先
                keys.remove(oldest);
                if(keys.isEmpty()){//如果删除了这个频次的相关信息后,再也没有这个频次所对应的key表了,直接就直接删除这个key表
                    freqToKeys.remove(this.minFreq);
                    //此处是否应该需要更新minFreq
                    //假如说需要更新minFreq,那么该操作的时间复杂度就不是O(1)了,而是O(n)
                    //我们将算法流程走一遍下来哈
                    //首先就是什么时候会走这个分支?
                    //是当需要进行置换,而且插入一个新key的时候才会被调用
                    //那么插入新key的时候,最小的访问频率是不是一定是1?
                    //那么就没有必要更新minFreq了
                }
                //更新key-val表,已经预处理完毕
                keyToVal.remove(oldest);
                //更新KF表
                keyToFreq.remove(oldest);
            }
            //否则未满则直接插入即可
            keyToVal.put(key,value);//将对应的值插入到主表
            keyToFreq.put(key,1);//初次访问设置为1
            //同时设置fk表
            freqToKeys.putIfAbsent(1,new LinkedHashSet<>());
            freqToKeys.get(1).add(key);
            //插入新key后的最小freq肯定是1
            this.minFreq = 1;
        }
    
        void increase(int key){
            //1.修改key=>freq表
            int freq = keyToFreq.get(key);
            keyToFreq.put(key,freq+1);
            //2.修改freq=>key表
            //2.1 将freq=>对应的key删除掉
            LinkedHashSet<Integer> keys = freqToKeys.get(freq);
            keys.remove(key);
            //将key加入到freq+1对应的列表里
            freqToKeys.putIfAbsent(freq+1,new LinkedHashSet<>());
            freqToKeys.get(freq+1).add(key);
            if(keys.isEmpty()){
                //那么就移除这个表
                freqToKeys.remove(freq);
                //如果这个freq恰好就是最小的,已经没有比它更小的了,符合逻辑,删除
                if(freq == this.minFreq){
                    this.minFreq++;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
  • 相关阅读:
    离职交接,心态要好
    3个实用性特别强的毕业论文选题思路
    C++ Reference: Standard C++ Library reference: C Library: cwchar: wcstok
    《TCP/IP网络编程》阅读笔记--Socket类型及协议设置
    FreeRTOS笔记 - 二(正点原子)
    C#: 解释器模式(附完整源码)
    Kubernetes:kube-apiserver 之启动流程(一)
    MECT4CNER 代码遇到的问题
    监控数据的采集方式及原理
    OpenCV快速入门:图像形态学操作
  • 原文地址:https://blog.csdn.net/weixin_50340097/article/details/126482093