持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
大厂高频算法面试题:《LFU 缓存淘汰算法》,您将学到 LFU 是什么?解决什么问题?现在的大厂必问的高频算法面试题,不要求能现场手撸出来,可以说明下 LFU 实现的具体思路和用到的数据结构模型。
一、LFU 是什么
LFU (least frequently used),即最少频率使用策略,在内存不够时,淘汰掉使用频率低的数据。
二、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) 的平均时间复杂度运行。
示例:
输入:
["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
三、LFU 实现
LFU 难度不在于算法上的设计,在于算法上的coding
在内存满时,淘汰掉频度低的数据,如果频度相同,则淘汰掉最早的数据(一直未被使用)
LFU 内存淘汰策略:
- 频度不一样,淘汰掉最低频度的数据
- 频度一样,淘汰掉最早的数据
二维双向链表
最左侧的桶次数是最少的
最右侧的桶次数是最多的
每个桶中存储的都是次数相同的元素
空桶要删除,避免内存泄漏,比如某个元素操作了100万次,如果不删除空桶,那会出现99万个空桶
为什么要用二维桶?就是为了删除记录时,要删谁,删除最左侧桶的第一条数据
至于怎么删除第一条数据,看具体链表实现,比如删除链表的头部数据(桶内频次相同,删除最早数据),那就要求添加数据从链表尾部添加,如果删除链表的尾部数据(桶内频次相同,删除最早数据),那就要求添加数据从链表头部添加
public class LFUCache {
MyLFUCache<Integer, Integer> myLFUCache;
public LFUCache(int capacity) {
this.myLFUCache = new MyLFUCache<>(capacity);
}
public int get(int key) {
Integer result = myLFUCache.get(key);
return result == null ? -1 : result;
}
public void put(int key, int value) {
myLFUCache.put(key, value);
}
// 节点的数据结构
public static class Node<K, V> { // 支持泛型
public K key;
public V value;
public Integer times; // 这个节点发生get或者set的次数总和
public Node<K, V> up; // 节点之间是双向链表所以有上一个节点
public Node<K, V> down;// 节点之间是双向链表所以有下一个节点
public Node(K key, V value, int times) {
this.key = key;
this.value = value;
this.times = times;
}
}
// 桶结构
public static class NodeList<K, V> { // 支持泛型
public Node<K, V> head; // 桶的头节点
public Node<K, V> tail; // 桶的尾节点
public NodeList<K, V> pre; // 桶之间是双向链表所以有前一个桶
public NodeList<K, V> next; // 桶之间是双向链表所以有后一个桶
public NodeList(Node<K, V> node) {
this.head = node;
this.tail = node;
}
// 把一个新的节点加入这个桶,新的节点都放在顶端变成新的头部
public void addNodeFromHead(Node<K, V> newHead) {
newHead.down = head;
head.up = newHead;
head = newHead;
}
// 判断这个桶是不是空的
public boolean isEmpty() {
return head == null;
}
// 删除node节点并保证node的上下环境重新连接
public void deleteNode(Node<K, V> 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;
}
}
// 总的缓存结构
public static class MyLFUCache<K, V> { // 支持泛型
private int capacity; // 缓存的大小限制,即K
private int size; // 缓存目前有多少个节点
private HashMap<K, Node<K, V>> records;// 表示key(Integer)由哪个节点(Node)代表
private HashMap<Node<K, V>, NodeList<K, V>> heads; // 表示节点(Node)在哪个桶(NodeList)里
private NodeList<K, V> headList; // 整个结构中位于最左的桶
public MyLFUCache(int K) {
this.capacity = K;
this.size = 0;
this.records = new HashMap<>();
this.heads = new HashMap<>();
headList = null;
}
// removeNodeList:刚刚减少了一个节点的桶
// 这个函数的功能是,判断刚刚减少了一个节点的桶是不是已经空了。
// 1)如果不空,什么也不做
//
// 2)如果空了,removeNodeList还是整个缓存结构最左的桶(headList)。
// 删掉这个桶的同时也要让最左的桶变成removeNodeList的下一个。
//
// 3)如果空了,removeNodeList不是整个缓存结构最左的桶(headList)。
// 把这个桶删除,并保证上一个的桶和下一个桶之间还是双向链表的连接方式
//
// 函数的返回值表示刚刚减少了一个节点的桶是不是已经空了,空了返回true;不空返回false
private boolean modifyHeadList(NodeList<K, V> removeNodeList) {
if (removeNodeList.isEmpty()) {
if (headList == removeNodeList) {
headList = removeNodeList.next;
if (headList != null) {
headList.pre = null;
}
} else {
removeNodeList.pre.next = removeNodeList.next;
if (removeNodeList.next != null) {
removeNodeList.next.pre = removeNodeList.pre;
}
}
return true;
}
return false;
}
// 函数的功能
// node这个节点的次数+1了,这个节点原来在oldNodeList里。
// 把node从oldNodeList删掉,然后放到次数+1的桶中
// 整个过程既要保证桶之间仍然是双向链表,也要保证节点之间仍然是双向链表
private void move(Node<K, V> node, NodeList<K, V> oldNodeList) {
oldNodeList.deleteNode(node);
// preList表示次数+1的桶的前一个桶是谁
// 如果oldNodeList删掉node之后还有节点,oldNodeList就是次数+1的桶的前一个桶
// 如果oldNodeList删掉node之后空了,oldNodeList是需要删除的,所以次数+1的桶的前一个桶,是oldNodeList的前一个
NodeList<K, V> preList = modifyHeadList(oldNodeList) ? oldNodeList.pre
: oldNodeList;
// nextList表示次数+1的桶的后一个桶是谁
NodeList<K, V> nextList = oldNodeList.next;
if (nextList == null) {
NodeList<K, V> newList = new NodeList(node);
if (preList != null) {
preList.next = newList;
}
newList.pre = 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<K, V> newList = new NodeList(node);
if (preList != null) {
preList.next = newList;
}
newList.pre = preList;
newList.next = nextList;
nextList.pre = newList;
if (headList == nextList) {
headList = newList;
}
heads.put(node, newList);
}
}
}
public void put(K key, V value) {
if (capacity == 0) {
return;
}
if (records.containsKey(key)) {
Node<K, V> node = records.get(key);
node.value = value;
node.times++;
NodeList<K, V> curNodeList = heads.get(node);
move(node, curNodeList);
} else {
if (size == capacity) {
Node<K, V> node = headList.tail;
headList.deleteNode(node);
modifyHeadList(headList);
records.remove(node.key);
heads.remove(node);
size--;
}
Node<K, V> 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.pre = newList;
headList = newList;
}
}
records.put(key, node);
heads.put(node, headList);
size++;
}
}
public V get(K key) {
if (!records.containsKey(key)) {
return null;
}
Node<K, V> node = records.get(key);
node.times++;
NodeList<K, V> curNodeList = heads.get(node);
move(node, curNodeList);
return node.value;
}
}
}