大家好我是苏麟 , 今天聊聊LRU问题 , 相信学过操作系统的小伙伴并不陌生 .
LRU问题
LRU的含义
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
图解 :
如果再有其他元素就依次类推。
LRU缓存
描述 :
设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。
它应该支持以下操作: 获取数据 get
和 写入数据 put
。
获取数据 get(key)
- 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value)
- 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
题目 :
LeetCode LRU缓存 :
面试题 16.25. LRU 缓存
分析 :
目前公认最好的方式是使用Hash+双链表 :
Hash的作用是 用来做到O(1)访问元素,哈希表就是普通的哈希映射 (HashMap),通过缓存数据的键映射到其在双向链表中的位置。Hash里的数据是key-value结构。value就是我们自己封装的node , key则是键值,也就是在Hash的地址 .
这里的头节点 , 尾节点起到辅助的作用 , 并没有什么实际意义 , 相当于虚拟节点 .
举例 :
我们先看容量为3的例子,首先缓存了1,此时结构如图a所示。之后再缓存2和3,结构如b所示。
之后 4再进入,此时容量已经不够了,只能将最远未使用的元素1删掉,然后将4插入到链表头部。此时就变成了上图c的样子。
接下来假如又访问了一次2,会怎么样呢? 此时会将2移动到链表的首部,也就是下图d的样子。
之后假如又要缓存5呢? 此时就将tail指向的3删除,然后将5插入到链表头部。也就是上图e的样子。上面的方案要实现是非常容易的 . 要注意链表的几个操作 :
.假如容量没满,则将新元素直接插入到链表头就行了
2.如果容量够了,新的元素到来,则将tail指向的表尾元素删除就行了
3.假如要访问已经存在的元素,则此时将该先从链表中删除,再插入到表头就行了
再看Hash的操作:
1.Hash没有容量的限制,凡是被访问的元素都会在Hash中有个标记,key就是我们的查询条件,而value就是链表的结点的引用,可以不用访问链表直接定位到某个结点,然后就可以执行我们在上一节提到的方法来删除对应的结点
2.这里双向链表的删除好理解,那HashMap中是如何删除的呢? 其实就是将node变成为null。这样get(key)的时候返回的是null,就实现了删除的功能
上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成[删除该节点]和[在双向链表的头部添加节点]两步操作,都可以在 O(1)时间内完成
方法 :
- moveNode() 删除节点
- removetail() 删除尾节点
- addNode() 添加头节点
- moveToHead() 移动到头节点
- get() 得到节点的值
- put() 插入节点
解析 :
class LRUCache {
class Node{
int key;
int val;
Node pre;
Node next;
public Node(){
}
public Node(int key , int val){
this.key = key;
this.val =val;
}
}
private Map<Integer,Node> map = new HashMap<>();
private Node head;
private Node tail;
private int size;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
head = new Node();
tail = new Node();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
Node temp = map.get(key);
if(temp == null){
return -1;
}
moveToHead(temp);
return temp.val;
}
public void put(int key, int value) {
Node temp = map.get(key);
if(temp == null){
Node node = new Node(key,value);
map.put(key,node);
addNode(node);
size++;
if(size > capacity){
Node delete = removetail();
map.remove(delete.key);
size--;
}
}else{
temp.val = value;
moveToHead(temp);
}
}
//移动到头节点
public void moveToHead(Node move){
moveNode(move);
addNode(move);
}
//添加头节点
public void addNode(Node temp){
temp.next = head.next;
temp.pre = head;
head.next.pre = temp;
head.next = temp;
}
//删除尾节点并返回删除的节点
public Node removetail(){
Node temp = tail.pre;
moveNode(temp);
return temp;
}
//删除节点
public void moveNode(Node temp){
temp.pre.next = temp.next;
temp.next.pre = temp.pre;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
这期就到这里 , 下期见!