赞
踩
提示:LUR和LFU原理都类似的
之前讲过LRUCache:
LRU缓存内存替换算法:least recently unused 缓存内存替换算法
现在来讲LFUCache
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
void put(int key, int value) - 如果键 key 已存在,则变更其值;
如果键不存在,请插入键值对。
当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。
在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。【LRU】
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。【频率times】
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。
对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lfu-cache
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例:
熟悉LRUCache的话,就知道LFU只需要在LRU上面多一个嵌入的frequency维度
即一个2维双向链表
看本题之前,一定把这个文章看了,了解LRU怎么实现的:
LRU缓存内存替换算法:least recently unused 缓存内存替换算法
看懂了LRU,就很容易理解LFU
不妨设capacity=3
put(A,17)
put(B,13)
put(C,1)
这3个都是操作频次为1的
现在这个双向链表就是LRU,按照使用时间顺序链接的,up和down指针相连的。
get©
C现在操作频次为2了,显然不是1,AB还是1,独立开来
其实现在LRFU由2个桶构成,桶1频次为1,桶2频次为2,
桶和桶之间是last和next指针链接
put(D,10)
此时ABC已经是满足容量了,所以要挤出去一个,挤出去谁呢?自然是频率count最低的那个桶(一个桶内就一个LRU),然后要删除head,操作就是LRU中的操作。
put(B,15)
然后,又操作B,B要去2频次的桶,而且比C晚一点
put(B,17)
再一次操作B,频次为3,自然要新增一个桶
count=1的频次最低,也即最左边的桶mostLeft
每次挤兑出去的一定是mostLeft中的head
这就是LFU
是不是有了LRU就能很容易理解?
看了案例就知道,这就是LRU上多了一个频次维度,仅此而已
当然,代码非常考验细节!!!
联系coding能力
我看的是左神的代码:
桶的内部,是一个二维的双向链表,每一个节点有2个指针,这个节点被操作了几次,用times记录
// 节点的数据结构
public static class Node {
public Integer key;
public Integer value;
public Integer times; // 这个节点发生get或者set的次数总和
public Node up; // 节点之间是双向链表所以有上一个节点
public Node down;// 节点之间是双向链表所以有下一个节点
public Node(int k, int v, int t) {
key = k;
value = v;
times = t;
}
}
LRU操作的是NDLL
而LFU操作的是桶Bucket,桶比NDLL多了last和next指针(在同种,NDLL的last和next被称为up和down)
(1)Bucket内部某一个桶,它有节点吗?没有就是空,需要有判断函数:isEmpty()
(2)给桶新加一个节点,放入头addNodeFromHead(x)
(3)删除桶中的某个节点:分为删除头,中间x和尾,分情况讨论,删除方法不一样
// 桶结构 public static class Bucket {//{}--{}--{}中的{}, // 它内部装了一串双向量表,有head和tail,它自己还有左右接指针last和next public Node head; // 桶的头节点 public Node tail; // 桶的尾节点 public Bucket last; // 桶之间是双向链表所以有前一个桶---这就是二维双向链表????牛 public Bucket next; // 桶之间是双向链表所以有后一个桶 public Bucket(Node node) {//新建一个桶,必须给一个节点,至少 head = node; tail = node; } // 把一个新的节点加入这个桶,新的节点都放在顶端变成新的头部, // 删除的时候就是最左侧桶的尾部,旧 public void addNodeFromHead(Node newHead) { newHead.down = head; head.up = newHead; head = newHead;//挪上去 } // 判断这个桶是不是空的 public boolean isEmpty() { return head == null; } // 删除桶中的node节点,并保证node的上下环境重新连接 public void deleteNode(Node node) { if (head == tail) {//一个节点,全部删除 head = null; tail = null; } else { //多个节点 if (node == head) {//是头 head = node.down; head.up = null; } else if (node == tail) {//是尾 tail = node.up; tail.down = null; } else {//中间节点,跳过直连 node.up.down = node.down; node.down.up = node.up; } } node.up = null; node.down = null;//断开node前后,表示消失 } }///桶结构结束
拿着这个桶,实现LFU
(1)LFU属性:
——capacity,最多能缓存几个点?容量
——size目前已经缓存了几个点?
——哈希表:keyNodeMap,key的代表节点Node:代表Father
——头桶headBucket(leftMost最左测那个桶)
(2)这个函数:boolean modifyHeadBucket(Bucket removeNodeBucket)
判断刚刚减少了一个节点的桶是不是已经空了。
removeNodeBucket:刚刚减少了一个节点的那个桶
——如果不空,什么也不做
——如果空了,假如removeNodeBucket还是整个缓存结构最左的桶(headBucket)。
则:删掉这个桶的同时,也要让最左的桶变成removeNodeBucket的下一个。
——如果空了,假如removeNodeBucket不是整个缓存结构最左的桶(headBucket)。
则:把这个桶删除,并保证上一个的桶和下一个桶之间还是双向链表的连接方式
函数的返回值表示刚刚减少了一个节点的桶是不是已经空了,空了返回true;不空返回false
(3)移动move(Node node, Bucket oldBucket) 函数
由于操作来了,node这个节点的次数+1了,这个节点原来在oldBucket里。既然频次增加了,就需要放到跟高一个频次的桶中,所以:
把node从oldBucket删掉,然后放到次数+1的桶中接入尾部
整个过程既要保证桶之间仍然是双向链表,也要保证节点之间仍然是双向链表
显然,oldBucket的上一个桶preBucket需要知道是谁?
oldBucket的下一个桶nextBucket也需要知道是谁?
(4)新加节点函数add(key,value)
——有了就是更新
——没有就是新加,容量多了,先移除最左边桶的头,然后加入最左边桶的尾部,和LRU一样
(5)获取key,get(x)
只需要将其移动到高频次那个桶
很复杂,了解核心思想即可:
//LFUCache数据结构-- //有了桶,桶中有一串链表,就可以整一个最左侧的桶,开始一个桶一个桶地往下挂接,形成一个二维链表 //第一维度:headBucket--{}--{}--{} //二维度:每一个桶中有一串链表:{head--Node-tail} private int capacity; // 缓存的大小限制,即K private int size; // 缓存目前有多少个节点 private HashMap<Integer, Node> keyNodeMap;// 表示key(Integer)由哪个节点(Node)代表 private HashMap<Node, Bucket> nodeBucketMap; // 表示节点(Node)在哪个桶(Bucket)里--重点 private Bucket headBucket; // 整个结构中位于最左的桶---最左侧的桶 // 本题测试链接 : https://leetcode.com/problems/lfu-cache/ // 提交时把类名和构造方法名改为 : LFUCache public Top89LFU(int K) {//初始化这个缓存机制的数据结构时,需要告诉缓存的容量K capacity = K; size = 0;//现在有几个节点? keyNodeMap = new HashMap<>(); nodeBucketMap = new HashMap<>(); headBucket = null; } // removeNodeBucket:刚刚减少了一个节点的桶 // 这个函数的功能是,判断刚刚减少了一个节点的桶是不是已经空了。 // 1)如果不空,什么也不做 // // 2)如果空了,removeNodeBucket还是整个缓存结构最左的桶(headBucket)。 // 删掉这个桶的同时也要让最左的桶变成removeNodeBucket的下一个。 // // 3)如果空了,removeNodeBucket不是整个缓存结构最左的桶(headBucket)。 // 把这个桶删除,并保证上一个的桶和下一个桶之间还是双向链表的连接方式 // // 函数的返回值表示刚刚减少了一个节点的桶是不是已经空了, // 空了返回true;不空返回false private boolean modifyHeadBucket(Bucket removeNodeBucket) { if (removeNodeBucket.isEmpty()) { //桶空了 if (headBucket == removeNodeBucket) { headBucket = removeNodeBucket.next;//更新最左侧的桶 if (headBucket != null) { headBucket.last = null;//断开之前的桶 } } else { //最左侧的桶,不是刚刚删除了一个节点的桶--跳过本桶直连前后,与链表类似 removeNodeBucket.last.next = removeNodeBucket.next; if (removeNodeBucket.next != null) {//确实后面还要,就要跳指 removeNodeBucket.next.last = removeNodeBucket.last; } } return true; } //不空无所谓 return false; } // 函数的功能 // node这个节点的次数+1了,这个节点原来在oldBucket里。 // 把node从oldBucket删掉,然后放到次数+1的桶中 // 整个过程既要保证桶之间仍然是双向链表,也要保证节点之间仍然是双向链表 private void move(Node node, Bucket oldBucket) { oldBucket.deleteNode(node); // preBucket表示次数+1的桶的前一个桶是谁 // 如果oldBucket删掉node之后还有节点,oldBucket就是次数+1的桶的前一个桶,就是node所在的桶 // 如果oldBucket删掉node之后空了, // oldBucket是需要删除的,所以次数+1的桶的前一个桶,是oldBucket的前一个 Bucket preBucket = modifyHeadBucket(oldBucket) ? oldBucket.last : oldBucket; // nextBucket表示次数+1的桶的后一个桶是谁 Bucket nextBucket = oldBucket.next; if (nextBucket == null) { Bucket newBucket = new Bucket(node);//因为操作频次+1了,右边没有桶的话,就需要生成一个再放入 if (preBucket != null) { preBucket.next = newBucket; } newBucket.last = preBucket; if (headBucket == null) { headBucket = newBucket; } nodeBucketMap.put(node, newBucket); } else {//后面的桶不是空的,有 if (nextBucket.head.times.equals(node.times)) {//而且频次确实是本次操作之后+1的频次 nextBucket.addNodeFromHead(node);//然后加进去就行 nodeBucketMap.put(node, nextBucket);//同步记录节点的地址 } else {//频次不一样,依然需要重新生成一个桶 Bucket newBucket = new Bucket(node); if (preBucket != null) { preBucket.next = newBucket; } newBucket.last = preBucket; newBucket.next = nextBucket; nextBucket.last = newBucket; if (headBucket == nextBucket) { headBucket = newBucket; } nodeBucketMap.put(node, newBucket); } } } //新加节点 public void put(int key, int value) { if (capacity == 0) { return; } if (keyNodeMap.containsKey(key)) {//修改值 Node node = keyNodeMap.get(key); node.value = value; node.times++; Bucket curNodeList = nodeBucketMap.get(node); move(node, curNodeList);//完成移动工作 } else { if (size == capacity) {//已经放满了节点,需要干掉之前的最低频的那个节点,然后才能新加 Node node = headBucket.tail; headBucket.deleteNode(node);//需要删除那个最久没用的 modifyHeadBucket(headBucket); keyNodeMap.remove(node.key); nodeBucketMap.remove(node); size--;//这样才有一个空间给新来的数据添加机会 } Node node = new Node(key, value, 1);//新来的频次为1 if (headBucket == null) { headBucket = new Bucket(node);//没有桶 } else { if (headBucket.head.times.equals(node.times)) { headBucket.addNodeFromHead(node);//最左边那个确实是为1频次 } else {//否则就要造一个1频次的桶 Bucket newBucket = new Bucket(node); newBucket.next = headBucket; headBucket.last = newBucket; headBucket = newBucket; } } keyNodeMap.put(key, node);//同步记录刚刚来的这个bucket和节点的关系 nodeBucketMap.put(node, headBucket); size++;//新增一条记录 } } //获取key public int get(int key) { if (!keyNodeMap.containsKey(key)) { return -1; } Node node = keyNodeMap.get(key);//有,操作频次++ node.times++; Bucket curNodeList = nodeBucketMap.get(node); move(node, curNodeList);//完成移动操作--当前所在的桶就是旧桶的位置 return node.value; } //思想要搞明白,大厂有人考过………………
提示:重要经验:
1)LFU就是LRU的扩展,然后多了频次这个维度,基础数据结构是桶:Bucket
2)核心思想就和LRU很相似,如果面试遇到的话说核心思想,然后尝试这沟通看看能否写其中一个函数,太复杂了这个玩意。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。