目录
LRUCache简介:
LRUCache的实现:
LinkedHashMap方法实现:
自己实现链表:
前言:
有需要本文章源码的友友请前往:LRUCache源码
LRUCache简介:
LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。 什么是Cache?狭义的Cache指的是位于CPU和主存间的快速RAM, 通常它不像系统主存那样使DRAM技术,而使用昂贵但较快速的SRAM技术。 广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache, 内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等。
Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实, LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。👍👍👍
LRUCache的实现:
实现LRUCache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)。
具体如下图,这个图一定要记住,下面的代码都是围绕这个图来展开的。🌸🌸🌸
LRUCache是实现主要有两种方法:
(1)是使用JDK中类似LRUCahe的数据结构LinkedHashMap。
(2)自己实现双向链表+HashMap实现。
LinkedHashMap方法实现:
由于LinkdeHashMap和LRUCache非常相似,故我们直接继承它,直接调用它的方法就行,其实就是给LinkedHashMap套了个LRUCache的壳。😎😎😎
基本参数:
private int capacity;
public LRUCache(int capacity){
super(capacity,0.75f,true);
this.capacity = capacity;
}
super()是调用父亲的构造方法(LinkdeHashMap),其源码如下:
参数说明:
1. initialCapacity 初始容量大小,使用无参构造方法时,此值默认是16。
2. loadFactor 加载因子,使用无参构造方法时,此值默认是 0.75f。
3. accessOrder 如果为false基于插入顺序(后续会解释),如果为true基于访问顺序。
那么什么是loadFactor呢?
loadFactor(负载因子)是表示Hash表中元素的填满的程度。🌸🌸🌸
accessOrder决定的顺序是做什么的?
当accessOrder为false时,下面代码打印map结果如下:🌸🌸🌸
当accessOrder为true时,下面代码打印map的结果如下:
通过对比两张图片我们不难发现在map执行get后 " 1 "被放到map的末尾。这个性质就很好的符合LRUCache的性质,所以我们可以使用LinkedHashMap来模拟实现LRUCache。
具体代码如下:
public class LRUCache extends LinkedHashMap<Integer,Integer> {
private int capacity;
public LRUCache(int capacity){
super(capacity,0.75f,true);
this.capacity = capacity;
}
public int get(int key){
return super.getOrDefault(key,-1);
}
public void put(int key,int value){
super.put(key,value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return super.size() > capacity;
}
}
基本都是调用LinkedHashMap的方法,故这里不再过多赘述,唯一需要注意的一点是:removeEldestEntry方法必须要重写,因为在其源码中是默认返回false。
下面有一道关于LRUCache的oj题目,友友们可以用实现后的代码去跑一跑。
LRU缓存
自己实现链表:
链表的节点,这里采用的是带头节点和带尾节点的双向链表(实现非常方便)。节点和HashMap对应,采用静态内部类。
static class DLinkNode{
public int key;//对应map的key
public int val;//对应map的value
public DLinkNode next;//后指针
public DLinkNode prev;//前指针
public DLinkNode(int key,int val){
this.key = key;
this.val = val;
}
public DLinkNode(){
}
}
LRUCache的全局变量及构造方法如下:这里需要注意要把head节点和tail节点之间相互指向一下,不然会空指针异常,顺便把HashMap初始好。
DLinkNode head;//头节点
DLinkNode tail;//尾节点
public int capacity;//LRUCache的容量
public int usedSize;//LRUCache中的节点个数
Map<Integer,DLinkNode> cache;
public MyLRUCache(int capacity){
this.capacity = capacity;
head = new DLinkNode();//创建头节点
tail = new DLinkNode();//创建尾节点
head.next = tail;//将头尾节点相互指向,防止空指针异常
tail.prev = head;
cache = new HashMap<>();
}
查找key:
首先利用哈希表以O(1)的时间复杂度完成查找key对应的节点,拿到节点后将其移动到尾节点同时返回对应的节点值。移动节点到尾节点分为两步,1.删除当前节点 2.尾插新节点。为了使代码更加简洁使用函数独立实现实现。
/**
* 查找key对应节点如果找不到返回-1,如果有将它放到尾节点
* @param key
* @return
*/
public int get(int key){
DLinkNode node = cache.get(key);
//不存在直接返回-1
if(node == null){
return -1;
}
//存在的情况
//将node节点移动到末尾
//1.删除当前节点
//2.尾插新节点
moveTail(node);
//3.返回值
return node.val;
}
moveTail源码如下:
下面都是一些简单的链表操作,画个图就没有什么问题了。
private void moveTail(DLinkNode node){
//1.删除node节点
remove(node);
//2.将node查到末尾
addToTail(node);
}
/**
* 将node节点添加到尾节点
* @param node
*/
private void addToTail(DLinkNode node){
tail.prev.next = node;
node.prev = tail.prev;
node.next = tail;
tail.prev = node;
}
/**
* 将node节点删除
* @param node
*/
private void remove(DLinkNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
效果如下:
插入节点:
对于一般数据结构来说插入和删除节点是所有基本操作中最难的,在LRUCache的插入中如果节点大于一个临界值的话,插入节点后要进行删除不常用的节点(头节点)😭😭😭。要分为节点已经存不存在两种情况来分情况讨论,如果已经存在的话就要更新节点对应的值,如果不存在的话先把节点插入末尾后再加入哈希表长度 + 1,当超过capacity是要把头节点删除同时要把它在哈希表的映射关系删除掉。🤩🤩🤩
public void put(int key,int val) {
DLinkNode node = cache.get(key);
if (node != null) {
//1.如果key已经存在
node.val = val;//更新节点的值
moveTail(node);//将node节点移动到尾节点
}else {
//2.如果key不存在
node = new DLinkNode(key, val);
addToTail(node);
cache.put(key, node);//将node节点添加到哈希表中
usedSize++;
//当超过LRUCache的容量
if (usedSize > capacity) {
DLinkNode removeNode = head.next;//要删除的节点,方便后续操作
removeHead(removeNode);//删除头节点
cache.remove(removeNode.key);
usedSize--;
}
}
}
删除同节点(removeHead)对应的源码如下:
画图画图画图🐳🐳🐳
private void removeHead(DLinkNode node){
head.next = node.next;
node.next.prev = head;
}
对应效果如下:这里可能在get(2)这里可能有的友友不太理解,这是因为get(1)后会把节点1放在尾节点,2就成头节点了,所以在插入3节点是删除是2节点而不是1节点。
一样的在实现完代码后可以拿到上面LinkedHashMap给出的例题去跑一跑。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。