java语言手动实现LFU算法策略及原理分析

今天我们通过java语言手动实现LFU算法策略及原理分析,这是redis缓存策略的一种,因此了解次算法及原很有必要;另外,leetcode 也有次题,介绍是这样的:

/**
 * 请你为 最不经常使用(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
 *
 *
 * 提示:
 *
 * 0 <= capacity <= 104
 * 0 <= key <= 105
 * 0 <= value <= 109
 * 最多调用 2 * 105 次 get 和 put 方法
 */

下面我们开始分析实现:

1、LFU是什么

 
LFU是一种淘汰算法(最近最少使用),全称是Least Frequently Used.

2、、LFU算法的思想

算法的思想就是:如果一个数据在最近一段时间内访问次数很少,
那么在将来它被访问的可能性也很小。
所以,当指定的空间已存满数据时,应当把访问次数很少的数据淘汰。
这里可以看出LRU和LFU的区别一个是访问数据的顺序,一个是数据的访问次数

3、LFU实现
根据算法思想实现需要满足已下条件:
1)新增key(默认访问次数1),当空间已存满数据,删除访问次数最少时间最长的key。
2)获取值时快速找到某个key是否存在,存在key(访问次数加+1),并返回其对应的 value。
3)多个key可能具有相同的访问次数

4、原理图:

 

5、存储原理:

6、核心实现代码:

 

import java.util.HashMap;
 
public class LFUCache {

    public static void main(String[] args) {
        LFUCache lfuCacheTask = new LFUCache(2);
        lfuCacheTask.put(1,1);
        lfuCacheTask.put(2,2);
        lfuCacheTask.get(1);
        lfuCacheTask.put(3,3);
        lfuCacheTask.get(2);
        lfuCacheTask.get(3);
        lfuCacheTask.put(4,4);
        lfuCacheTask.get(1);
        lfuCacheTask.get(3);
        lfuCacheTask.get(4);
    }

    MyLFUCache<Integer,Integer> myLFUCache;

    public LFUCache(Integer capacity){
        myLFUCache = new MyLFUCache<>(capacity);
    }

    public int get(Integer key){
        Node<Integer, Integer> node = myLFUCache.get(key);
        return node == null ? -1 : node.value;
    }

    public void put(Integer key,Integer value){
        myLFUCache.set(key,value);
    }

    public static class Node<K,V>{
        private K key;
        private V value;
        private Integer mapKey=1;//此节点被操作的次数
        private Node<K,V> next;
        private Node<K,V> last;

        public Node() {}

        public Node(K key,V value){
            this.key = key;
            this.value = value;
        }
    }

    public static class DoubleNodeList<K,V>{
        private Node head;
        private  Node tail;
        /**
         * 双向链表初始化
         */
        public DoubleNodeList(){
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.last = head;
        }

        /**
         * 删除最后一个节点
         */
        private void removeLastNode(){
            if(tail.last == head){
                return;
            }
            removeNode(tail.last);
        }

        /**
         * 删除双向链表的某个节点(首、尾、或者其他节点通用)
         * @param node
         * @return
         */
        private void removeNode(Node<K,V> node){
            node.last.next = node.next;
            node.next.last = node.last;
        }

        /**
         * 添加到尾节点
         * @param newNode
         */
        private void addToTail(Node<K,V> newNode){
            newNode.next = tail;
            newNode.last = tail.last;
            tail.last.next = newNode;
            tail.last = newNode;
        }

        /**
         * 同理,在某个节点前面添加一个节点也是如此逻辑
         * @param current
         * @param newNode
         */
        private void addBeforeNode(Node<K,V> current,Node<K,V> newNode){
            newNode.last = current.last;
            newNode.next = current;
            current.last.next = newNode;
            current.last = newNode;
        }

        /**
         * 添加到首节点
         * @param newNode
         */
        private void addToHead(Node<K,V> newNode){
            newNode.next = head.next;
            newNode.last = head;
            head.next.last = newNode;
            head.last = newNode;
        }

        /**
         * 同理,在某个节点后面添加一个节点也是如此逻辑
         * @param current
         * @param newNode
         */
        private void addAfterNode(Node<K,V> current,Node<K,V> newNode){
            newNode.next = current.next;
            newNode.last = current;
            current.next.last = newNode;
            current.next = newNode;
        }
    }

    public static class MyLFUCache<K,V>{
        private HashMap<K,Node<K,V>> cacheMap;
        private HashMap<Integer,DoubleNodeList<K,V>> incMap;
     //   private DoubleNodeList<K,V> nodeList;
        private final int capacity;
        int min;

        public MyLFUCache(int capacity) {
            cacheMap = new HashMap<>();
            incMap = new HashMap<>();
          //  nodeList = new DoubleNodeList<>();
            this.capacity = capacity;
        }

        public void set(K key,V value){
            if (capacity == 0) {
                return;
            }
            Node<K, V> kvNode = cacheMap.get(key);
            if(kvNode != null){//已经添加过次节点数据
                kvNode.value = value;
                freInc(kvNode);
            }else {//首次添加
                if(cacheMap.size() == capacity){//如果达到容量阈值,需要先删除,单位时间内使用的次数最少的节点数据
                    DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(min);
                    cacheMap.remove(kvDoubleNodeList.head.next.key);
                    //删除首节点,即最长时间没有用到的(单位时间内操作次数相同的情况下)
                    kvDoubleNodeList.removeNode(kvDoubleNodeList.head.next);
                }
                Node<K,V> newNode = new Node<>(key,value);
                cacheMap.put(key,newNode);
                //查询并创建首次操作的记录
                DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(1);
                if(kvDoubleNodeList == null){
                    kvDoubleNodeList = new DoubleNodeList<>();
                    incMap.put(1,kvDoubleNodeList);
                }
                //添加到尾节点,即最近时间用到的数据
                kvDoubleNodeList.addToTail(newNode);
                min = 1;
            }
        }

        public Node<K, V> get(K key){
            Node<K, V> kvNode = cacheMap.get(key);
            if (kvNode == null) {
                return null;
            }
            freInc(kvNode);
            return kvNode;
        }

        private void freInc(Node<K,V> kvNode) {
            Integer mapKey = kvNode.mapKey;//找到单位时间内操作的次数
            DoubleNodeList<K, V> kvDoubleNodeList = incMap.get(mapKey);//找到改此次对应的所有的节点数据
            kvDoubleNodeList.removeNode(kvNode);//删除改节点,后面会加到新的 mapKey+1 对应的链表里
            //判断次链表里是否还有数据
            if(min == mapKey && kvDoubleNodeList.head.next == kvDoubleNodeList.tail){
                min++;
            }
            mapKey++;
            kvNode.mapKey++;
            kvDoubleNodeList = incMap.get(mapKey);//先找,再存
            if(kvDoubleNodeList == null){
                kvDoubleNodeList = new DoubleNodeList<>();
                incMap.put(mapKey,kvDoubleNodeList);
            }
            kvDoubleNodeList.addToTail(kvNode);
        }
    }
}

测试代码注释比较全,多多测试思考,定会很快掌握。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值