面试最常考的算法—LRU和LFU
本题来自leetcode。这个方法是我自己写的,思路十分清晰。
1.LRU题目
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
- 实现思路
LRU淘汰缓存机制,是通过时间顺序去淘汰的,查看相应的次序。常用的在前边的,不常用的在后边,当容量不够就删除后用的。所以用链表实现,当中间某一个key又被使用的时候需要掉换在最前面。更好的去删除,和修改。
使用双链表和hash表来实现。题目很简单,使用LinkedHashMap是java的常用类中提供的LUR算法。使用类很简单但是肯定不是面试官想要的。leetcode官方都这么说的
实现本题的两种操作,需要用到一个哈希表和一个双向链表。在面试中,面试官一般会期望读者能够自己实现一个简单的双向链表,而不是使用语言自带的、封装好的数据结构。在 Python 语言中,有一种结合了哈希表与双向链表的数据结构 OrderedDict,只需要短短的几行代码就可完成本题。在 Java 语言中,同样有类似的数据结构 LinkedHashMap。
这些做法都不会符合面试官的要求,而不多做任何阐述。
class LRUCache {
// 存放键 和结点
HashMap<Integer,Node> map = new HashMap<>();
// 链表来实现缓存的先后顺序进行增删
DoubleList list = new DoubleList();
// 定义容量,进行初始化
int cap ;
public LRUCache(int capacity) {
this.cap = capacity ;
}
public int get(int key) {
// 查询map中如果有没有这个键就返回-1
if(!map.containsKey(key)){
return -1 ;
}
// 当存在时,一定要将其放在首位,所以从map中拿到节点,在链表中删除,然后新加
Node node = map.get(key);
list.remove(node);
list.addLast(node);
return node.value;
}
public void put(int key, int value) {
// 先将节点新建
Node node = new Node(key,value);
// 查看节点的key是否存在,当存在的时候说明链表中已经有这个值了所以直接删除,在新增的时候就自动放在首位了,
if(map.containsKey(key)){
list.remove(map.get(key));
}
if(cap == list.size()){
// 当内存溢出的时候要移除首位元素。但是不要忘了在map中也要移除。
Node first = list.removeFirst();
map.remove(first.key);
}
//
list.addLast(node);
// 当键存在就更新,不存在就新加
map.put(key,node);
}
// 双向链表节点的类
class Node{
int key , value ;
Node pre ,next ;
public Node(int key ,int value){
this.key = key;
this.value = value;
}
}
// 双向链表的实现
class DoubleList{
int size ;
Node head ,tail;
public DoubleList(){
head = new Node(0,0);
tail = new Node(0,0);
this.size = 0 ;
this.head.next = tail ;
this.tail.pre = head ;
}
public void addLast(Node x){
x.pre = tail.pre;
x.next =tail;
tail.pre.next = x ;
tail.pre = x ;
size++ ;
}
public void remove(Node x){
x.next.pre = x.pre ;
x.pre.next = x.next;
size--;
}
public Node removeFirst(){
Node first = head.next;
if(first == tail){
return null ;
}
remove(first);
return first;
}
public int size(){
return size;
}
}
}
2. LFU题目
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
- 实现思路
LFU是利用使用的次数的多少来衡量石否删除。而LRU是通过使用的次序也就是时间顺序。
所以要实现LFU,首先得有一个表一个存储数据的表HashMap<Integer,Integer> keyToVal
其次就是得有一个表用来计算次数 HashMap<Integer,Integer> keyToFreq
。
当内存满的时候,需要删除相应的次数较少的key,所以必须有一个minFaareqKey
来记录。
其次是为了好找最低次数避免遍历,所以需要一个次数对应key的一个表HashMap<Integer,LinkedHashSet<Integer>
class LFUCache {
// key 到 val的映射
HashMap<Integer,Integer> keyVal;
// key 到 freq 的映射
HashMap<Integer,Integer> keyToFreq;
// freq到key的映射
HashMap<Integer,LinkedHashSet<Integer>> ferqToKey;
int minFreq;
int cap ;
// 初始化
public LFUCache(int capacity) {
keyVal = new HashMap<>();
keyToFreq = new HashMap<>();
ferqToKey = new HashMap<>();
minFreq = 0;
cap = capacity;
}
public int get(int key) {
// 判断是否存在
if(!keyVal.containsKey(key)){
return -1;
}
// 存在的时候,将次数和相应的表自增
increaseFreq(key);
// 返回相应的数值
return keyVal.get(key);
}
public void put(int key, int value) {
if(this.cap <= 0){
return ;
}
// 在填充的时候查看是否key已经存在,如果存在,则更新数值keyToVal表,次数增加
if(keyVal.containsKey(key)){
keyVal.put(key,value);
increaseFreq(key);
return;
}
// 如果不存在,在添加的时候判断一下看容器是否满了
if(cap == keyVal.size()){
// 满了就删除次数少的key
removeMinFreqKey();
}
// 如果没满就添加,并且将次数置为1,
keyVal.put(key,value);
keyToFreq.put(key,1);
// 判断1次对应的key 有没有值就是null,没有就需要新建容器再添加。LinkedHashSet<>()底层使用hash表可以用通过key ,很快定位删除
ferqToKey.putIfAbsent(1,new LinkedHashSet<>());
ferqToKey.get(1).add(key);
// 每次新增就意味者最小次数就又置为1 了
this.minFreq = 1;
}
// 次数+1时所需要更新的表
public void increaseFreq(int key){
// 现得到现在的key对应的次数
int freq = keyToFreq.get(key);
// 更新keyToFreq表,将次数+1
keyToFreq.put(key,freq+1);
// freqToKey表中将之前freq次数所在的集合的键删除,在freq+1所对应的集合把key新增,在新增的时候看集合是否存在,不存在就先创建。
ferqToKey.get(freq).remove(key);
ferqToKey.putIfAbsent(freq+1,new LinkedHashSet<>());
ferqToKey.get(freq+1).add(key);
// 当原来的集合删除键之后应该判断一下,是否为空,为空就删除相应的次数所在的集合。
if(ferqToKey.get(freq).isEmpty()){
ferqToKey.remove(freq);
// 当删除的集合是最小次数的集合时,就将最小次数+1
if(freq == minFreq ){
this.minFreq++;
}
}
}
public void removeMinFreqKey(){
LinkedHashSet<Integer> list = ferqToKey.get(minFreq);
int deleteKey = list.iterator().next();
list.remove(deleteKey);
if(list.isEmpty()){
ferqToKey.remove(this.minFreq);
}
keyVal.remove(deleteKey);
keyToFreq.remove(deleteKey);
}
}