题目链接
思路:双向链表+哈希表
分析:
这是一个很经典的策略,很多缓存淘汰策略都是采用的这个。
这里还要熟悉指针的操作。
这个题目的意思就是:
- 当键值对的个数没达到capacity的时候,那么就直接加入缓存。
- 当键值对的个数达到了capacity的时候,那么就要淘汰一个键值对。
这里存储键值对使用的是双向链表+哈希表。
双向链表(DouList)的结构如下:
class DouList{
//指向头节点
DouListNode head;
//指向尾节点
DouListNode tail;
//双向链表的总容量节点数
int cap;
//双向链表中实际的节点数
int total;
public DouList(int capacity){
head=null;
tail=null;
cap=capacity;
}
}
双向链表中有头指针和尾指针,是为了方便操作。并且记录着容量的上限。
双向链表的节点(DouListNode)的数据结构如下:
class DouListNode{
//用于在hash中删除key-val对
int key;
int val;
//前置节点
DouListNode pre;
//后置节点
DouListNode after;
public DouListNode(int key, int val){
this.key = key;
this.val=val;
}
}
节点中记录着键值对(key-val),并且记录着前置节点和后置节点。
哈希表中的key:就是DouListNode中的key,
哈希表中的val:就是DouLikstNode
这里有两个问题:
问题1:当键值对的个数没有达到capacity的时候,是直接加入缓存,但是这里是要加入到最近使用的第一个位置。因为这是最近使用的。即加入时候是直接加入到双向链表的头部的。
. . . 问题1.1:加入的时候还要分两种情况
… …1. 链表中存在key值,那么要更新val值。
… …2. 链表中不存在key值,那么就是真正的插入。
问题2:当键值的个数达到了上限的时候,应该淘汰一个键值对,并且是双向链表的尾部节点。
然后又回到了第一个问题了,插入这个节点,因为刚刚已经淘汰了一个键值对,所以此时就又回到了没有到达上限的情况。
这里复杂一点的情况是:当插入节点的时候,这个节点已经存在了链表中。如图:
双向链表中已经存在了键值对(2-2),此时插入键值对(2-5)。
步骤:
-
首先通过哈希表判断是否存在这个key,这里判断出来是存在。
-
那么取出这个DouListNode,并且得到这个节点的前置节点。也就是键值对(1-1)。
-
将前置节点指向当前节点的后置节点。如下图
-
并且将后置节点的前置指针指向当前节点的前置节点。如下图。
-
此时,删除原来的键值对(2-2)节点,就成了如下图。
-
此时将要插入的节点(2-5)插到头节点位置,也就是要实现一个头插法。
将当前节点的后置指针指向原本的头节点。如下图
-
将原来的头节点的前置指针指向当前节点。如图。
-
将头指针指向当前节点。完毕。如图。
上面只是举例了一种较为复杂点的情况。完整代码如下。
代码:
class LRUCache {
//双向链表----
//Hash表的val存的是Node
class DouListNode{
//用于在hash中删除key-val对
int key;
int val;
//前置节点
DouListNode pre;
//后置节点
DouListNode after;
public DouListNode(int key, int val){
this.key = key;
this.val=val;
}
}
class DouList{
//指向头节点
DouListNode head;
//指向尾节点
DouListNode tail;
//双向链表的总容量节点数
int cap;
//双向链表中实际的节点数
int total;
public DouList(int capacity){
head=null;
tail=null;
cap=capacity;
}
}
private HashMap<Integer, DouListNode> map;
private DouList dlist;
public LRUCache(int capacity) {
map=new HashMap<>();
dlist = new DouList(capacity);
}
//每次get完,key不存在就不说了
//将ket所对应的node节点放到头部,也就是头节点
public int get(int key) {
DouListNode node = map.get(key);
if(node==null){
return -1;
}
//将node放到头部节点去,并且长度不变
changePlaceToHead(node);
return node.val;
}
//将key-val放到map中,将node放到头节点
public void put(int key, int value) {
DouListNode node = new DouListNode(key,value);
if(this.dlist.head==null){
this.dlist.head = node;
this.dlist.tail = node;
this.dlist.total++;
map.put(key, node);
return;
}else if(this.dlist.cap==1){
map.remove(this.dlist.head.key);
this.dlist.head = node;
this.dlist.tail = node;
this.dlist.total++;
map.put(key, node);
return;
}
if(!map.containsKey(key)){
//此时本身没有包含key
if(this.dlist.total<this.dlist.cap){
//双向链表还没放满 节点数+1
this.dlist.total++;
}else{
//满了,删除最后一个节点,长度依旧是不变的,删除最后一个节点,又放入了一个节点,所以总的长度没变
//倒数第二个节点
DouListNode tailPre = this.dlist.tail.pre;
if(tailPre!=null){
tailPre.after = null;
}
//从map中删除key-val对
map.remove(this.dlist.tail.key);
//调整尾部节点为倒数第二个节点
this.dlist.tail = tailPre;
}
//将新的放进去
map.put(key, node);
//当前node就是新的头节点
node.after = this.dlist.head;
this.dlist.head.pre = node;
this.dlist.head = node;
}else{
node = map.get(key);
node.val = value;
changePlaceToHead(node);
}
}
//将node放到头部去
private void changePlaceToHead(DouListNode node){
//前置节点
DouListNode preNode = node.pre;
if(preNode==null){
//说明自身就是头部节点
return;
}
//后置节点
DouListNode afterNode = node.after;
//将前置节点的后置指针 指向 node的后置节点
preNode.after = afterNode;//preNode->afterNode
if(afterNode!=null){
//如果后置节点不为null,那么将后置节点的前置指针 指向 node的前置节点
afterNode.pre = preNode;// preNode<-afterNode
}else{
//如果当前节点的后置指针为null,说明node就是尾节点。那么移走了当前node,那么尾节点应该是当前节点的前置节点
this.dlist.tail = preNode;//tail=preNode
}
//node为head,那么后置指针应该指向 以前的head,前置指针 为null
node.after = this.dlist.head;// ->head
this.dlist.head.pre = node;// <-head
node.pre = null;
this.dlist.head = node;//head=node
}
}
/**
* 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);
*/