LFU 的设计与实现
LFU 的设计与实现
作者:Grey
原文地址:
题目描述
LFU(least frequently used)。即最不经常使用页置换算法。
主要思路
首先,定义一个辅助数据结构 Node
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;
}
}
这个 Node 用于封装 LFU Cache 每次加入的元素,其中 key 和 value 两个变量记录每次加入的 KV 值,times 用于记录该 KV 值被操作(get/set)的次数之和, up 和 down 两个变量用于链接和 KV 出现词频一样的数据项,用链表串联。
接下来需要另外一个辅助数据结构 NodeList,前面的 Node 结构已经把词频一致的数据项组织在同一个桶里,这个 NodeList 用于连接出现不同词频的桶,用双向链表组织
public static class NodeList {
public Node head; // 桶的头节点
public Node tail; // 桶的尾节点
public NodeList last; // 桶之间是双向链表所以有前一个桶
public NodeList next; // 桶之间是双向链表所以有后一个桶
public NodeList(Node node) {
head = node;
tail = node;
}
……
}
使用一个具体的示例来表示上述两个结构如何组织的
例如,LFU Cache 在初始为空的状态下,进来如下数据
key = A, value = 3
key = B, value = 30
key = C, value = 4
key = D, value = 12
那么 LFU 会做如下组织

此时只有出现一次的桶,接下来,如果 key = C 这条记录 被访问过了,所以词频变为2,接下来要把 key = C 这条记录先从词频为1的桶里面取出来,然后再新建一个词频为 2 的桶,把这个 key = C 的数据项挂上去,结果如下

接下来,如果又操作了 key = C 这条记录,那么这条记录的词频就是 3, 又需要新增一个词频为 3 的桶,原来词频为 2 的桶已经没有数据项了,要销毁,并且把词频为 1 的桶和词频为 3 的桶连接在一起。

接下来,如果操作了 key = A,则 key = A 成为词频为 2 的数据项,再次新增词频为 2 的桶,并把这个桶插入到词频为 1 和词频为 3 的桶之间,如下图

以上示例就可以很清楚说明了 Node 和 NodeList 两个数据结构在 LFU 中的作用,接下来,为了实现快速的 put 和 get 操作,需要定义如下成员变量
int capacity; // 缓存的大小限制
int size; // 缓存目前有多少个节点
HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表
HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
NodeList headList; // 整个结构中位于最左的桶,是一个双向链表
说明:records 这个变量就是用于快速得到某个 key 的节点(Node)是什么,由于这里的 kv 都是整型,所以用 Integer 作为 key 可以定位到对应的 Node 数据项信息。
heads 则用于快速定位某个 Node 在哪个桶里面。
headList 表示整个结构中位于最左侧的桶,这个桶一定是出现次数最少的桶,所以淘汰的时候,优先淘汰这个桶里面的末尾位置,即 tail 位置的 node!
两个核心方法 put 和 get 的核心代码说明如下
public void put(int key, int value) {
if (records.containsKey(key)) {
// put 的元素是已经存在的
// 更新元素值,更新出现次数
Node node = records.get(key);
node.value = value;
node.times++;
// 通过heads以O(1)复杂度定位到所在的桶
NodeList curNodeList = heads.get(node);
// 把这个更新后的 Node 从 旧的桶迁移到新的桶
move(node, curNodeList);
} else {
if (size == capacity) {
// 容量已经满了
// 淘汰 headList 尾部的节点!因为这个节点是最久且最少用过的节点
Node node = headList.tail;
headList.deleteNode(node);
// 删掉的节点有可能会让 headList 换头,因为最右侧的桶可能只有一个节点,被删除后,就没有了。
modifyHeadList(headList);
// records和 heads 中都要删掉其记录
records.remove(node.key);
heads.remove(node);
size--;
}
// 以上操作就是淘汰了一个节点
// 接下来就放心加入节点
// 先建立Node,词频设置为 1
Node node = new Node(key, value, 1);
if (headList == null) {
// 如果headList为空,说明最左侧的桶没有了,新来节点正好充当最左侧节点的桶中元素
headList = new NodeList(node);
} else {
if (headList.head.times.equals(node.times)) {
// 最右侧桶不为空的情况下,这个节点出现的次数又正好等于最左侧桶所代表的节点数
// 则直接加入最左侧桶中
headList.addNodeFromHead(node);
} else {
// 将加入的节点作为做左侧桶,接上原先的headList
// eg:新加入的节点出现的次数是1,原先的 headList代表的桶是词频为2的数据
// 就会走这个分支
NodeList newList = new NodeList(node);
newList.next = headList;
headList.last = newList;
headList = newList;
}
}
records.put(key, node);
heads.put(node, headList);
size++;
}
}
public int get(int key) {
if (!records.containsKey(key)) {
// 不包含这个key
// 按题目要求直接返回 -1
return -1;
}
// 否则,先取出这个节点
Node node = records.get(key);
// 词频+1
node.times++;
// 将这个节点所在的桶找到
NodeList curNodeList = heads.get(node);
// 将这个节点从原桶调整到新桶
move(node, curNodeList);
return node.value;
}
PS:这里涉及的对双向链表和桶链表的两个操作move和modifyHeadList逻辑不难,但是很多繁琐的边界条件要处理,具体方法的说明见上述代码注释,不赘述。
完整代码如下
static class LFUCache {
private int capacity; // 缓存的大小限制
private int size; // 缓存目前有多少个节点
private HashMap<Integer, Node> records; // 表示key(Integer)由哪个节点(Node)代表
private HashMap<Node, NodeList> heads; // 表示节点(Node)在哪个桶(NodeList)里
private NodeList headList; // 整个结构中位于最左的桶
public LFUCache(int capacity) {
this.capacity = capacity;
size = 0;
records = new HashMap<>();
heads = new HashMap<>();
headList = null;
}
// 节点的数据结构
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;
}
}
// 桶结构
public static class NodeList {
public Node head; // 桶的头节点
public Node tail; // 桶的尾节点
public NodeList last; // 桶之间是双向链表所以有前一个桶
public NodeList next; // 桶之间是双向链表所以有后一个桶
public NodeList(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;
}
}
private boolean modifyHeadList(NodeList removeNodeList) {
if (removeNodeList.isEmpty()) {
if (headList == removeNodeList) {
headList = removeNodeList.next;
if (headList != null) {
headList.last = null;
}
} else {
removeNodeList.last.next = removeNodeList.next;
if (removeNodeList.next != null) {
removeNodeList.next.last = removeNodeList.last;
}
}
return true;
}
return false;
}
private void move(Node node, NodeList oldNodeList) {
oldNodeList.deleteNode(node);
NodeList preList = modifyHeadList(oldNodeList) ? oldNodeList.last : oldNodeList;
NodeList nextList = oldNodeList.next;
if (nextList == null) {
NodeList newList = new NodeList(node);
if (preList != null) {
preList.next = newList;
}
newList.last = preList;
if (headList == null) {
headList = newList;
}
heads.put(node, newList);
} else {
if (nextList.head.times.equals(node.times)) {
nextList.addNodeFromHead(node);
heads.put(node, nextList);
} else {
NodeList newList = new NodeList(node);
if (preList != null) {
preList.next = newList;
}
newList.last = preList;
newList.next = nextList;
nextList.last = newList;
if (headList == nextList) {
headList = newList;
}
heads.put(node, newList);
}
}
}
public void put(int key, int value) {
if (capacity == 0) {
return;
}
if (records.containsKey(key)) {
Node node = records.get(key);
node.value = value;
node.times++;
NodeList curNodeList = heads.get(node);
move(node, curNodeList);
} else {
if (size == capacity) {
Node node = headList.tail;
headList.deleteNode(node);
modifyHeadList(headList);
records.remove(node.key);
heads.remove(node);
size--;
}
Node node = new Node(key, value, 1);
if (headList == null) {
headList = new NodeList(node);
} else {
if (headList.head.times.equals(node.times)) {
headList.addNodeFromHead(node);
} else {
NodeList newList = new NodeList(node);
newList.next = headList;
headList.last = newList;
headList = newList;
}
}
records.put(key, node);
heads.put(node, headList);
size++;
}
}
public int get(int key) {
if (!records.containsKey(key)) {
return -1;
}
Node node = records.get(key);
node.times++;
NodeList curNodeList = heads.get(node);
move(node, curNodeList);
return node.value;
}
}
更多
参考资料
LFU 的设计与实现的更多相关文章
- 2017-4-25/设计缓存(LFU)
1. 恒定缓存性能有哪些因素? 命中率.缓存更新策略.缓存最大数据量. 命中率:指请求缓存次数和缓存返回正确结果次数的比例.比例越高,缓存的使用率越高,用来衡量缓存机智的好坏和效率.如果数据频繁更新, ...
- 缓存的设计及PHP实现LFU
1. 恒定缓存性能有哪些因素? 命中率.缓存更新策略.缓存最大数据量. 命中率:指请求缓存次数和缓存返回正确结果次数的比例.比例越高,缓存的使用率越高,用来衡量缓存机智的好坏和效率.如果数据频繁更新, ...
- 缓存子系统如何设计(Cachable tag, Memcache/redis support, xml config support, LRU/LFU/本地缓存命中率)
大家对这段代码肯定很熟悉吧: public List<UserInfo> SearchUsers(string userName) { string cacheKey=string.For ...
- 页面置换算法 - FIFO、LFU、LRU
缓存算法(页面置换算法)-FIFO. LFU. LRU 在前一篇文章中通过leetcode的一道题目了解了LRU算法的具体设计思路,下面继续来探讨一下另外两种常见的Cache算法:FIFO. LFU ...
- 缓存算法(页面置换算法)-FIFO、LFU、LRU
在前一篇文章中通过leetcode的一道题目了解了LRU算法的具体设计思路,下面继续来探讨一下另外两种常见的Cache算法:FIFO.LFU 1.FIFO算法 FIFO(First in First ...
- LeetCode之LRU Cache 最近最少使用算法 缓存设计
设计并实现最近最久未使用(Least Recently Used)缓存. 题目描述: Design and implement a data structure for Least Recently ...
- cache 的设计与实现--转载
本文整理自一下两篇博客:http://my.oschina.net/ScottYang/blog/298727http://my.oschina.net/u/866190/blog/188712 Ca ...
- [LeetCode] Design In-Memory File System 设计内存文件系统
Design an in-memory file system to simulate the following functions: ls: Given a path in string form ...
- 面试挂在了 LRU 缓存算法设计上
好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存 ...
- 【缓存算法】FIFO,LFU,LRU
一.FIFO算法 FIFO(First in First out),先进先出.其实在操作系统的设计理念中很多地方都利用到了先进先出的思想,比如作业调度(先来先服务),为什么这个原则在很多地方都会用到呢 ...
随机推荐
- POJ2282 The Counting Problem(数位DP)
用dp[pos][val][cnt]表示状态,pos是数位,val是当前统计的数字,cnt是目前统计的目标数字的出现次数 注意状态的转移过程,统计数字0时前导0的影响. 1 #include<c ...
- Docker | 容器互联互通
上篇讲到创建自定义网络,我创建了 mynet 网络,并指定了网关和子网地址.在上篇结尾呢,我抛出了一个问题:其它网络下的容器可以直接访问mynet网络下的容器吗?今天就让我们一块看下怎么实现容器互联. ...
- 一天五道Java面试题----第六天(1)
这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 1.mybatis和hibernate的对比 2 .#{}和${}的区别 3 .mybatis插件运行原理及开发流程 4 ...
- vue中使用分页组件、将从数据库中查询出来的数据分页展示(前后端分离SpringBoot+Vue)
文章目录 1.看实现的效果 2.前端vue页面核心代码 2.1. 表格代码(表格样式可以去elementui组件库直接调用相应的) 2.2.分页组件代码 2.3 .script中的代码 3.后端核心代 ...
- 齐博x1第三季《模板风格的制作》系列009-自定义区块代码
本节来说明如何自定义区块代码,不再继承上层模板,实现个性模板 上一节因为我们继承了layout布局模版,所以我们自定义的代码就无效了 如果我们继承了上层模板,那么相当于我们复制了一份上层模板的结构,也 ...
- python: m个位置,每个位置有n种可能,求所有排列结果
>>> import itertools >>> m, n = 3, 2 >>> paths = list(itertools.product(r ...
- python更改文件后缀名
path = '1024.png' extension = 'jpg' for i in range(1,len(path)): if (path[-i] == '.'):#找到后缀初始点 new_p ...
- OpenStack云计算平台框架
概: OpenStack是包含很多独立组件的一个云计算平台框架.在安装组件前,需要先将框架搭建出来,才能向其中放置组件. 搭建open stack云计算平台框架 一.安装open stack云计算平 ...
- maven 重复依赖不同版本 选择规则
maven 重复依赖不同版本 选择规则 本篇主要来看看 maven 对于 重复依赖的jar的不同版本时候 它内部的选择规则, 很多时候我们在搭建环境的时候 不注意就会存在依赖冲突等问题 那依赖冲突的时 ...
- JWT中token的理解
今天我们来聊一聊关于JWT授权的事情. JWT:Json Web Token.顾名思义,它是一种在Web中,使用Json来进行Token授权的方案. 既然没有找好密码,token是如何解决信任问题的呢 ...