【❤重点❤】【数据结构系列】——LRU缓存淘汰算法
LRU缓存淘汰算法
LRU 算法实际上是让你设计数据结构:
首先要接收一个 capacity 参数作为缓存的最大容量, 然后实现两个 API, 一个是 put(key, val) 方法存入键值对, 另一个是 get(key) 方法获取 key 对应的 val, 如果 key 不存在则返回-1。
LRU 缓存算法的核心数据结构就是哈希链表, 双向链表和哈希表的结合体。
cache这个数据结构必备条件:
- 显然cache中的元素必须有时序,以区分最近使用的和长久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置
- 要在cache中快速找某个key是否存在并得到对应的val
- 每次访问cache中的某个key,需要将这个元素变为最近使用的,也就是说cache要支持在任意位置快速插入和删除元素。

结合这个数据结构,分析上述三个条件:
- 每次默认从链表尾部添加元素,那么显然越靠尾部的元素越是最近使用的,越靠头部的元素就是越久未使用的
- 对于某一个key,可以通过哈希表快速定位到链表中的节点,从而取得对应的val
- 链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,这里借助哈希表,可以通过key快速映射到任意一个链表节点,然后进行插入和删除
问题:
-
为什么是双向链表,单向链表不行吗?
因为删除一个节点不仅要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证时间复杂度为O(1) -
既然哈希表已经存在了key,为什么链表中还要存key和val,只有val不行吗
因为在移除最少使用的元素中,需要删除的节点,而不是节点的值。
也就是说,当缓存容量已满,不仅要删除最后一个Node节点,还要把map中映射到该节点的key同时删除,而这个key只能由Node得到。如果Node结构中只存储val,那么无法得知key是什么,也就无法删除map中的键,造成错误。
双链表的节点类:
/**
* 定义双链表的节点类
*/
class Node{
public int key,val;
public Node next,prev;
public Node(int k,int v){
this.key = k;
this.val = v;
}
}
依靠Node类型构建双链表:
/**
* 依靠Node构建一个双链表
*/
class DoubleList{
//头尾虚节点
private Node head,tail;
//链表元素数
private int size;
public DoubleList(){
//初始化双向链表的数据
head = new Node(0,0);
tail = new Node(0,0);
head.next = tail;
tail.prev = head;
size = 0;
}
//在链表尾部添加节点x,时间复杂度为O(1)
public void addLast(Node x){
x.prev = tail.prev;
x.next = tail;
tail.prev.next = x;
tail.prev = x;
size++;
}
//删除链表中x节点(x一定存在)
//由于是双链表且给的是目标Node节点,时间复杂度为O(1)
public void remove(Node x){
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
//删除链表中第一个节点,并返回该节点,时间复杂度为O(1)
public Node removeFirst(){
if (head.next == tail)
return null;
Node first = head.next;
remove(first);
return first;
}
//返回链表长度,时间复杂度O(1)
public int size(){
return size;
}
}
把双向链表和哈希表结合起来,并实现LRU的 get() 和 put() 方法:
/**
* 构建LRU cache,把双向链表和哈希表结合起来
*/
class LRUCache{
//key -> Node(key,val)
private HashMap<Integer,Node> map;
//Node(k1,v1) <-> Node(k2,v2)....
private DoubleList cache;
//最大容量
private int cap;
public LRUCache(int capacity){
this.cap = capacity;
map = new HashMap<>();
cache = new DoubleList();
}
/**
* 将某个key提升为最近使用
*/
private void makeRecently(int key){
Node x = map.get(key);
//先从链表中删除这个节点
cache.remove(x);
//重新插到队尾
cache.addLast(x);
}
/**
* 添加最近使用的元素
*/
private void addRecently(int key,int val){
Node x = new Node(key,val);
//链表尾部就是最近使用的元素
cache.addLast(x);
//别忘了在map中添加key的映射
map.put(key,x);
}
/**
* 删除某一个key
*/
private void deleteKey(int key){
Node x = map.get(key);
//从链表中删除
cache.remove(x);
//从map中删除
map.remove(key);
}
/**
* 删除最久未使用的元素
*/
private void removeLeastRecently(){
//链表头部的第一个元素就是最久未使用的
Node deleteNode = cache.removeFirst();
//别忘了从map中删除它的key
map.remove(deleteNode.key);
}
/**
* 实现LRU的get方法
*/
public int get(int key){
if (!map.containsKey(key)){
return -1;
}
//将数据提升为最近使用的
makeRecently(key);
return map.get(key).val;
}
/**
* 实现 LRU的put方法
*/
public void put(int key,int val){
if (map.containsKey(key)){
//删除旧的数据
deleteKey(key);
//新插入的数据为最近使用的数据
addRecently(key,val);
return;
}
//如果容量已满
if (cap == cache.size){
//删除最久未使用的元素
removeLeastRecently();
}
//添加为最近使用的元素
addRecently(key,val);
}
}
使用java内置类型LinkedHashMap来实现LRU算法
package labuladong_learn.Data_structure;/**
* Copyright (C), 2019-2021
* author candy_chen
* date 2021/4/9 10:49
*
* @Classname LRU_2
* Description: 使用java内置类型LinkedHashMap来实现LRU算法
*/
import java.util.LinkedHashMap;
/**
*
*/
public class LRU_2 {
static class LRUCache{
int cap;
LinkedHashMap<Integer,Integer> cahce = new LinkedHashMap<>();
public LRUCache(int capacity){
this.cap = capacity;
}
public int get(int key){
if (!cahce.containsKey(key)){
return -1;
}
//将key变为最近使用
makeRecently(key);
return cahce.get(key);
}
public void put(int key,int val){
if (cahce.containsKey(key)){
//修改key值
cahce.put(key,val);
//将key变为最近使用
makeRecently(key);
return;
}
if (cahce.size() >= this.cap){
//链表头部就是最久未使用的key
int oldestKey = cahce.keySet().iterator().next();
cahce.remove(oldestKey);
}
//将新的key添加到链表尾部
cahce.put(key,val);
}
private void makeRecently(int key) {
int val = cahce.get(key);
//删除key,重新插入到队尾
cahce.remove(key);
cahce.put(key,val);
}
}
}

浙公网安备 33010602011771号