「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」
前言
笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。
系列文章收录《算法》专栏中。
LRU算法还是比较常见的,比如redis内存空间有限会自动清理最近最少使用的数据,还有mybatis的本地缓存也有LruCaches实现类。
问题描述
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类:
- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
- 1 <= capacity <= 3000
- 0 <= key <= 10000
- 0 <= value <= 105
- 最多调用 2 * 10^5 次 get 和 put
确定学习目标
对《LRU缓存算法》的算法过程进行剖析。
剖析
我们对问题的要求一一想办法解决:
- LRUCache对象创建的时候需要设置capacity用于控制容量:这个比较简单,LRUCache配置成员capacity(进行设置容量)、size(记录目前存放的元素的个数),在put的时候需要比较capacity和size的大小。
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1:这个也不困难但是get的平均时间复杂度需要控制在O(1),我们很容易相当实用HashMap去存放,就算hash碰撞也只要遍历链表和走红黑。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字:前半部分HashMap都已经实现,后半部分我们和get放在第四点进行描述。
- get之后对应元素变成最近最多实用的、put之后对应元素变成最近最多实用的、put超过capacity之后需要淘汰最久未使用的关键字:我们也比较容易想到是不是可以使用双向队列,刚get或者put的元素放在队列头部,超过capacity之后直接淘汰队列尾部的。
下面直接上代码
代码
package com.study.algorithm.lru;
import java.util.HashMap;
import java.util.Map;
public class LRUCache {
/**
* 双向列表节点
*/
class DLinkedNode {
private int key;
private int value;
/**
* 节点前面一个节点
*/
private DLinkedNode prev;
/**
* 节点下一个节点
*/
private DLinkedNode next;
public DLinkedNode() {
}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
/**
* 使用hashmap作为缓存,key为key,value为DLinkedNode
*/
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
/**
* 当前cache大小
*/
private int size;
/**
* cache容量
*/
private int capacity;
/**
* 头部,cache实例化的时候就进行初始化,固定有头部
*/
private DLinkedNode head;
/**
* 尾部,cache实例化的时候就进行初始化,固定有尾部
*/
private DLinkedNode tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.head = new DLinkedNode();
this.tail = new DLinkedNode();
this.head.next = this.tail;
this.tail.prev = this.head;
}
/**
* 根据key获得,没有返回-1,有返回value,并且把对应节点移动到头部
*
* @param key
* @return
*/
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
//从链表中删除该节点并移动到头部
moveToHead(node);
return node.value;
}
/**
* 有就重新赋值value并移动到头部,没有就添加。
* 需要注意判断size是否大于等于capacity,true的话就从cache和链表中移除,false的话就添加cache并且向头部添加该node
*
* @param key
* @param value
*/
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
//如果cache的大小超过容量就删除
if (size >= capacity) {
cache.remove(tail.prev.key);
removeNode(tail.prev);
--size;
}
DLinkedNode newNode = new DLinkedNode(key, value);
addToHead(newNode);
cache.put(key, newNode);
++size;
} else {
node.value = value;
moveToHead(node);
}
}
/**
* 从链表中去除该节点
*
* @param node
*/
public void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
/**
* 把该节点增加到头部
*
* @param node
*/
public void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
/**
* 从链表中删除该节点并移动到头部
*
* @param node
*/
public void moveToHead(DLinkedNode node) {
//先删除
removeNode(node);
//后增加到头部
addToHead(node);
}
public static void main(String[] args) {
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
}
}