460.LFU 缓存

请你为 最不经常使用(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) 的平均时间复杂度运行。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/lfu-cache
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路:

这次的数据由 键值对和计数君 三合一。因此我们应该定义一个结点类NodeLFU,用来存放键值对和计数君。 同时阅读题目我们了解到这个缓存机制--是达到满容量的时候会删除最久最不经常使用的数据,以此腾出空间。为了能快速删除最不经常使用的数据,就会想到用链表来存放数据,为了能更好的处理结点,链表应该为双链表。还有为了能快速更新相同 键的数据,将会引入一个哈希表来存放键的值和封装它的结点数据。

双链表的代码和结点的代码如下。为了更好的管理双链表,我们在双链表里面加入了几个方法,一个是链表的构造器,初始化头结点和尾结点;一个是头插法,加入最新的数据;还有一个是删除最久最不常使用的结点;为了能得到链表的数据量,外加一个size用来记录结点数

class DoubleListOfLFU{
    NodeLFU head;
    NodeLFU tail;
    int size;

    public DoubleListOfLFU() {
        head = new NodeLFU();
        tail = new NodeLFU();
        head.next = tail;
        tail.prev = head;
        size = 0;
    }
    public void addHead(NodeLFU node){
        head.next.prev = node;
        node.next = head.next;
        head.next = node;
        node.prev = head;
        size++;
    }
    public NodeLFU delLeast(){
        NodeLFU node = head.next;
        node.prev.next = node.next;
        node.next.prev = node.prev;
        size--;
        return node;
    }
}

class NodeLFU{
    NodeLFU next;
    NodeLFU prev;
    int key;
    int value;
    int calculate;

    public NodeLFU() {
        calculate = 0;
    }

    public NodeLFU(int key, int value) {
        this.key = key;
        this.value = value;
        calculate = 0;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

主要算法:

在LFUCahe类中定义一个哈希表来存放键和数据。在get或put一个键时 ,要更新put后的值,并将对应的calculate(计数君)增加,相对应的结点更新它在链表中的位置:

        将它移动到相同计数值的结点最后:比如,当有相同计数值1,2,2,2,3,3,3,4时,此时get了第一个2,那么计数君2就会+1 变成 3 , 同时因为它是最近使用的数据,我们就要把它更新到原来的第三个3后面,第一个4前面。

       在想通这一点后,我就开始实践

        做法一:在链表类中加上一个交换相邻结点的方法,并在put 和 get 方法里面循环调用,但是结果,力扣的测试用例最后一个给我整了一个10000个数据容量的缓存池,告诉我超时......转念一想,一定是交换相邻结点速度太慢了,于是更改思路

        做法二:交换相邻结点太慢了,那么就在链表中找到有相同计数君的最后一个结点,再把更新完的结点插入到后面去不就好了吗?但是有一次超时了。这个时候我才发现题目中的:所有方法的时间复杂度都应该为O(1)。此路不通,另寻他法

        做法三:为了能够找到相同计数次数的最后一个结点,就想到应该再加入一个哈希表countMap,来存放计数君和有这个计数君的最新的结点,只有相同计数次数最新的结点才有资格享受加入countMap这个无上荣耀,这个哈希表将会在每次put 和 get的时候更新。

        那么我们的update函数应运而生,传入的参数为刚更新的结点:

        如果countMap里面没有元素的时候,我们就要在put的时候在countMap中加入该数据,并退出;

        如果这个结点是calculate++前的相同计数次数的最后一个结点,就要将 原来的荣誉退让给次新的数据,然后自己再变成另一个计数次数的最新结点。用last存放 这个结点应该插入的位置的前一个结点。

        剩余代码如下,同时附上一些测试用例:

public class LFUCache {
    HashMapmap;
    HashMapcountMap;
    DoubleListOfLFU list;
    int cap ;

    public static void main(String[] args) {
        //测试用例
        //        LFUCache lfuCache = new LFUCache(3);
//        lfuCache.put(2,2);
//        lfuCache.put(1,1);
//        System.out.println(lfuCache.get(2));
//        System.out.println(lfuCache.get(1));
//        System.out.println(lfuCache.get(2));
//        lfuCache.put(3,3);
//        lfuCache.put(4,4);
//        System.out.println(lfuCache.get(3));
//        System.out.println(lfuCache.get(2));
//        System.out.println(lfuCache.get(1));
//        System.out.println(lfuCache.get(4));

//        LFUCache lfuCache = new LFUCache(2);
//        lfuCache.put(2,1);
//        lfuCache.put(3,2);
//        System.out.println(lfuCache.get(3));
//        System.out.println(lfuCache.get(2));
//        lfuCache.put(4,3);
//        System.out.println(lfuCache.get(2));
//        System.out.println(lfuCache.get(3));
//        System.out.println(lfuCache.get(4));

        LFUCache lfuCache = new LFUCache(2);
        lfuCache.put(3,1);
        lfuCache.put(2,1);
        lfuCache.put(2,2);
        lfuCache.put(4,4);
        System.out.println(lfuCache.get(2));

    }

    public LFUCache(int capacity) {
        cap = capacity;
        map = new HashMap<>();
        list = new DoubleListOfLFU();
        countMap = new HashMap<>();
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            NodeLFU node = map.get(key);
            node.calculate++;
            update(node);
            return node.value;
        } else {
            return -1;
        }
    }

    public void put(int key, int value) {
        if(map.containsKey(key)){
            NodeLFU node = map.get(key);
            node.setValue(value);
            node.calculate++;
            update(node);
        }else {
            NodeLFU newNode = new NodeLFU(key,value);
            if(list.size==cap){
                if(list.head.next!=list.tail) {
                    NodeLFU node = list.delLeast();
                    map.remove(node.key);
                    if(node==countMap.get(node.calculate)){
                        countMap.remove(node.calculate);
                    }
                }else return;
            }
            list.addHead(newNode);
            newNode.calculate++;
            update(newNode);
            map.put(key,newNode);
        }
    }

    public void update(NodeLFU node){
        //如果countMap里面没有元素,就要加入这个值
        if(countMap.isEmpty()){
            countMap.put(node.calculate,node);
            return;
        }
        if(countMap.get(node.calculate-1)==node){
            //node本身是上一个count的最后一个结点
            if(node.calculate-1==node.prev.calculate){
                //还有和 node 这个结点原先计数相同的计数次数
                //就更新上一个结点为前一个次数的尾结点
                countMap.put(node.calculate-1,node.prev);
            }else {
                //记录次数的哈希表中没有node原来的计数次数
                countMap.remove(node.calculate-1);
            }
        }
        //如果node不是尾结点的前一个结点
        if(!(node.next == list.tail)) {
            //如果last不存在,则要查看node是否在相同calculate或者前一个calculate的最后一个
            int temp = node.calculate;
            while(countMap.get(temp)==null&&temp>1){
                temp--;
            }
            NodeLFU last = countMap.get(temp);
            // 说明不用改变链表结构了,直接修改countMap就可以
            if (last != null) {
                //删除自身
                node.prev.next = node.next;
                node.next.prev = node.prev;
                //插入到last之后
                last.next.prev = node;
                node.next = last.next;
                last.next = node;
                node.prev = last;
            }
        }
        countMap.put(node.calculate,node);
    }
}

       通过测试用例之后神清气爽,实在是改了太久了。

        代码还是有点冗长,仍然可以优化。

460.LFU 缓存_第1张图片

 

你可能感兴趣的:(缓存,java,算法,leetcode,数据结构)