请你为 最不经常使用(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; HashMap countMap; 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); } }
通过测试用例之后神清气爽,实在是改了太久了。
代码还是有点冗长,仍然可以优化。