LRU是least Recently Used 的缩写,最近使用数据是热门数据,下一次大概率会再次使用。而最近少使用的数据,大概率下次也用不到,当缓存容量满的时候,优先淘汰最近少使用的数据。简单来说就是缓存容量满时,丢掉最久不用的数据。
假设容量为3 ,按照顺序插入键值对 [1-1],[2-2], [3-3],此时缓存中就是 :
- [3-3]
- [2-2]
- [1-1]
当我们再插入[4-4]时缓存中就是 : - [4-4]
- [3-3]
- [2-2]
此时[1-1]就被淘汰了,当我们此时在查询[2-2] 缓存中就是 : - [2-2]
- [4-4]
- [3-3]
键值对存储方面,我们可以使用「哈希表」来确保插入和查询的复杂度为 。
另外我们还需要额外维护一个「使用顺序」序列。
我们期望当「新数据被插入」或「发生键值对查询」时,能够将当前键值对放到序列头部,这样当触发 LRU 淘汰时,只需要从序列尾部进行数据删除即可。
*为了期望在*O(1)复杂度内调整摸个节点的位置,自然就想到了双向链表
具体的,我们使用哈希表来存储「键值对」,键值对的键作为哈希表的 Key,而哈希表的 Value 则使用我们自己封装的 Node 类,Node 同时作为双向链表的节点。
插入:检查当前键值对是否已经存在于哈希表:
如果存在,则更新键值对,并将当前键值对所对应的 Node 节点调整到链表头部
如果不存在,则检查哈希表容量是否已经达到容量:
没达到容量:插入哈希表,并将当前键值对所对应的 Node 节点调整到链表头部
已达到容量:先从链表尾部找到待删除元素进行删除,然后再插入哈希表,并将当前键值对所对应的 Node 节点调整到链表头部
查询:如果没在哈希表中找到该 Key,直接返回 ;如果存在该 Key,则将对应的值返回,并将当前键值对所对应的 Node 节点调整到链表头部
一些细节:
为了减少双向链表左右节点的「判空」操作,我们预先建立两个「哨兵」节点 head (头)和 tail(尾)。
//链表的节点
public static class Node{
public Node pre;//前一个节点
public Node next;//后一个节点
public int key;//查询 的键
public int value;//插入的值
public Node(int key, int value) {
this.key = key;
this.value = value;
}
public Node(){}
}
public class LRUTest {
Node head,tail;//建立两个「哨兵」节点 head (头)和 tail(尾)
int capacity;//缓存容量
int size;//缓存当前存入的个数
Map<Integer,Node> map;//定义一个Map key就对应这个value中也就是节点Node的key
public LRUTest(int capacity){
this.capacity = capacity;
newLinkedList();//初始化链表
size = 0;
map= new HashMap<>(capacity+2);
}
//初始化链表
private void newLinkedList() {
head = new Node();//头节点
tail = new Node();//尾节点
head.next = tail;//头节点的后一个节点就是尾节点
tail.pre = head;//尾节点的前一个节点就是头节点
}
}
这样我们就可以往中间插入节点(数据)了
//get操作先查询是否有key存在,如果存在把当前节点删掉,移动到头节点「哨兵」的后面
public int get(int key){
Node node = null;
if (cache.containsKey(key)){
goToHead(node);
return node.value;
}
return -1;
}
public void put(int key,int value){
//查询map中是否有对应key的node节点,如果node不等于空
//说明有,然后value重新赋值,把当前节点删掉,移动到头节点「哨兵」的后面
Node node = map.get(key);
if (node!=null){
node.value = value;
goToHead(node);
return;
}
//当容量满了尾节点「哨兵」的前一个节点将他删除
if (size == capacity){
Node lastNode = tail.pre;
deleteNode(lastNode);
cache.remove(lastNode.key);
size--;
}
//如果没有查到那么久新建一个节点放到头节点「哨兵」的后面
Node newNode = new Node();
newNode.key = key;
newNode.value = value;
addNode(newNode);
cache.put(key,newNode);
size++;
}
private void goToHead(Node node) {
//先删掉当前节点
deleteNode(node);
//添加到头节点
addNode(node);
}
//添加到头节点
private void addNode(Node node) {
head.next.pre = node;
node.next = head.next;
node.pre = head;
head.next = node;
}
//删掉当前节点
private void deleteNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
deleteNode操作
addNode操作
public class LRUTest {
Node head,tail;
int capacity;
int size;
Map<Integer,Node > map;
public LRUTest(int capacity){
this.capacity = capacity;
newLinkedList();
size = 0;
map= new HashMap<>(capacity+2);
}
private void newLinkedList() {
head = new Node ();
tail = new Node ();
head.next = tail;
tail.pre = head;
}
public static class Node{
public Node pre;
public Node next;
public int key;
public int value;
public Node (int key, int value) {
this.key = key;
this.value = value;
}
public Node(){
}
}
public int get(int key){
Node node = null;
if (cache.containsKey(key)){
node = cache.get(key);
goToHead(node);
return node.value;
}
return -1;
}
public void put(int key,int value){
Node node = cache.get(key);
if (node!=null){
node.value = value;
goToHead(node);
return;
}
if (size == capacity){
Node lastNode = tail.pre;
deleteNode(lastNode);
cache.remove(lastNode.key);
size--;
}
Node newNode = new Node ();
newNode.key = key;
newNode.value = value;
addNode(newNode);
cache.put(key,newNode);
size++;
}
private void goToHead(Node node) {
deleteNode(node);
addNode(node);
}
private void addNode(Node node) {
head.next.pre = node;
node.next = head.next;
node.pre = head;
head.next = node;
}
private void deleteNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
public static void main(String[] args) {
LRUTest lruTest = new LRUTest(3);//设置容量是3
lruTest.put(1,8);//此时缓存中是[1-8]
lruTest.put(2,3);//此时缓存中是[2-3],[1-8]
System.out.println(lruTest.get(1));//此时缓存中是[1-8],[2-3]输出8
lruTest.put(3,6);//此时缓存中是[3-6],[1-8],[2-3]
lruTest.put(4,9);//此时缓存中是[4-9],[3-6],[1-8]
System.out.println(lruTest.get(2));//此时缓存中是[4-9],[3-6],[1-8]没有key为2的所以返回-1
//控制台输出的就是8 和-1
}
}